俗话说得好:预先善其事,必先利其器,作为一个程序员,经常会用到 GitHub、Google、Stack Overflow 啥的,由于国内政策原因,想要访问国外网站就得科学上网,最常见的工具就是 ShadowsocksR,又被称为酸酸乳、SSR、小飞机,目前市面上有很多很多的机场,价格也不是很高,完全可以订阅别人的,但是订阅别人的,数据安全没有保障,有可能你的浏览历史啥的别人都能掌握,别人也有随时跑路的可能,总之,只有完全属于自己的东西才是最香的!
VPS(Virtual Private Server)即虚拟专用服务器技术,在购买 VPS 服务器的时候要选择国外的,推荐 Vultr,国际知名,性价比比较高,最低有$2.5/月、$3.5/月的,个人用的话应该足够了。
点击链接注册 Vultr 账号:https://www.vultr.com/?ref=8367048,目前新注册用户充值10刀可以赠送50刀,注册完毕之后来到充值页面,最低充值10刀,可以选择支付宝或者微信支付。
充值完毕之后,点击左侧 Products,选择服务器,一共有16个地区的,选择不同地区的服务器,最后的网速也有差别,那如何选择一个速度最优的呢?很简单,你可以一次性选择多个服务器,都部署上去,搭建完毕之后,测试其速度,选择最快的,最后再把其他的都删了,可能你会想,部署多个,那费用岂不是很贵,这里注意,虽然写的是多少钱一个月,而实际上它是按照小时计费的,从你部署之后开始计费,$5/月 ≈ $0.00694/小时,你部署完毕再删掉,这段时间的费用很低,可以忽略不计,一般来说,日本和新加坡的比较快一点,也有人说日本和新加坡服务器的端口封得比较多,容易搭建失败,具体可以自己测试一下,还有就是,只有部分地区的服务器有$2.5/月、$3.5/月的套餐,其中$2.5/月的只支持 IPv6,可以根据自己情况选择,最后操作系统建议选择 CentOS 7 x64 的,后面还有个 Enable IPv6 的选项,对 IPv6 有需求的话可以勾上,其他选项就可以不用管了。
部署成功后,点 Server Details 可以看到服务器的详细信息,其中有 IP、用户名、密码等信息,后面搭建 SSR 的时候会用到,此时你可以 ping 一下你的服务器 IP,如果 ping 不通的话,可以删掉再重新开一个服务器。
我们购买的是虚拟的服务器,因此需要工具远程连接到 VPS,如果是 Mac/Linux 系统,可以直接在终端用 SSH 连接 VPS:
1 | ssh root@你VPS的IP -p 22 (22是你VPS的SSH端口) |
如果是 Windows 系统,可以用第三方工具连接到 VPS,如:Xshell、Putty 等,可以百度下载,以下以 Xshell 为例:
点击文件,新建会话,名称可以随便填,协议为 SSH,主机为你服务器的 IP 地址,点击确定,左侧双击这个会话开始连接,最开始会出现一个 SSH安全警告,点击接受并保存即可,然后会让你输入服务器的用户名和密码,直接在 Vultr 那边复制过来即可,最后看到 [root@vultr ~]#
字样表示连接成功。
连接成功后执行以下命令开始安装 SSR:
1 | wget --no-check-certificate https://freed.ga/github/shadowsocksR.sh; bash shadowsocksR.sh |
如果提示 wget :command not found
,可先执行 yum -y install wget
,再执行上述命令即可。
执行完毕后会让你设置 SSR 连接密码和端口,然后按任意键开始搭建。
搭建成功后会显示你服务器 IP,端口,连接密码,协议等信息,这些信息要记住,后面使用 ShadowsocksR 的时候要用到。
由于我们购买的服务器位于国外,如果遇到上网高峰期,速度就会变慢,而锐速就是一款专业的连接加速器,可以充分利用服务器带宽,提升带宽吞吐量,其他还有类似的程序如 Google BBR 等,可以自行比较其加速效果,以下以操作系统为 CentOS 6&7 锐速的安装为例。
如果你服务器操作系统选择的是 CentOS 6 x64,则直接执行以下命令,一直回车即可:
1 | wget --no-check-certificate -O appex.sh https://raw.githubusercontent.com/hombo125/doubi/master/appex.sh && bash appex.sh install '2.6.32-642.el6.x86_64' |
如果你服务器操作系统选择的是 CentOS 7 x64,则需要先执行以下命令更换内核:
1 | wget --no-check-certificate -O rskernel.sh https://raw.githubusercontent.com/hombo125/doubi/master/rskernel.sh && bash rskernel.sh |
如下图所示表示内核更换完毕,此时已经断开与服务器的连接,我们需要重新连接到服务器,再执行后面的操作:
重新连接到服务器后,再执行以下命令:
1 | yum install net-tools -y && wget --no-check-certificate -O appex.sh https://raw.githubusercontent.com/0oVicero0/serverSpeeder_Install/master/appex.sh && bash appex.sh install |
然后一直回车即可,系统会自动安装锐速。
出现以下信息表示安装成功:
常见的工具有 ShadowsocksR、SSTap(原本是个游戏加速器,现在已经停止维护,但 GitHub 上仍然可以找到)等。
Shadowsocks 官网:https://shadowsocks.org/
ShadowsocksR 下载地址:https://github.com/Anankke/SSRR-Windows
SSTap GitHub 地址:https://github.com/FQrabbit/SSTap-Rule
不管什么工具,用法都是一样的,添加一个新的代理服务器,服务器 IP、端口、密码、加密方式等等这些信息保持一致就行了。然后就可以愉快地科学上网了!
经过以上步骤我们就可以科学上网了,但是目前为止只有一个端口,只能一个人用,那么如何实现多个端口多人使用呢?事实上端口、密码等信息是储存在一个叫做 shadowsocks.json
文件里的,如果要添加端口或者更改密码,只需要修改此文件即可。
连接到自己的 VPS,输入以下命令,使用 vim 编辑文件:vi /etc/shadowsocks.json
原文件内容大概如下:
1 | { |
增加端口,我们将其修改为如下内容:
1 | { |
也就是删除原来的 server_port
和 password
这两项,然后增加 port_password
这一项,前面是端口号,后面是密码,注意不要把格式改错了!!!修改完毕并保存!!!
接下来配置一下防火墙,同样的,输入以下命令,用 vim 编辑文件:vi /etc/firewalld/zones/public.xml
初始的防火墙只开放了最初配置 SSR 默认的那个端口,现在需要我们手动加上那几个新加的端口,注意:一个端口需要复制两行,一行是 tcp,一行是 udp。
原文件内容大概如下:
1 | <?xml version="1.0" encoding="utf-8"?> |
修改后的内容如下:
1 | <?xml version="1.0" encoding="utf-8"?> |
修改完毕并保存,最后重启一下 shadowsocks,然后重新载入防火墙即可,两条命令如下:
1 | /etc/init.d/shadowsocks restart |
1 | firewall-cmd –reload |
完成之后,我们新加的这几个端口就可以使用了
另外还可以将配置转换成我们常见的链接形式,如:ss://xxxxx
或 ssr://xxxxx
,其实这种链接就是把 IP,端口,密码等信息按照一定的格式拼接起来,然后经过 Base64 编码后实现的,有兴趣或者有需求的可以自行百度。
SSR 常用命令:
启动:/etc/init.d/shadowsocks start
停止:/etc/init.d/shadowsocks stop
重启:/etc/init.d/shadowsocks restart
状态:/etc/init.d/shadowsocks status
卸载:./shadowsocks-all.sh uninstall
更改配置参数:vim /etc/shadowsocks-r/config.json
还记得小时候写作文,畅想2020会怎样怎样,光阴似箭,2020真的来了,度过了艰难的考试周,抽了个晚上,回想了一下,决定写一写总结吧,似乎以前都没写过呢,那干脆连带2017、2018也写写吧,重点写一写2019的,以后争取每年都做一下总结。
2017年高三,上半年就不用说了,所有高三考生都一个样吧,下半年考进了武汉的某二本院校,软件工程专业,现在回想起来,当时把时间浪费得太多了,最开始加了一个部门,后来退了(事实上啥也学不到,浪费时间 ),然后除了完成学校的课程以外,其他啥也没搞,剩下的时间基本上全拿来骑车了,从高一开始就热爱单车运动,刚上大学肯定得放飞自我了,没课的时候就天天和学长到处跑,都快把武汉跑了个遍了,当时还定了个计划,大学四年骑车去一次西藏或者青海湖,其他的什么都没想,也没有对以后具体干哪方面做过规划,这一年收获最多的应该就是路上的风景了。
2018上半年,大一下学期,学习方面就过了个英语四级,然后依旧热衷于我的单车,暑假的时候疯狂了一把,7天干了700多公里,从学校骑回家了,那个时候正是热的时候,白天基本上在三十度,从武汉往西边走,后面全是爬山,上山爬不动,下山刹不住,路上也遇到了不少牛逼人物,有徒步西藏的,有环游中国的,直播平台有好几十万粉丝的……遇到的人都很善良,很硬汉,这次经历从某种程度上来说也是一次成长吧,一次很有意义的骑行。
下半年,也就是大二开始,才慢慢开始重视专业知识的学习,大二上学期搭建了个人博客,开始尝试写博客,其实就是把博客当做笔记吧,记性不好,学了的东西容易忘记,忘记了可以经常翻自己博客再复习复习,自己踩过的坑也记录记录,后来没想到有些文章访问量还挺高的,在博客搭建方面也帮到了一些网友,最重要的是结识了不少博友,有各行各业的大佬,下半年也定了方向,开始专注Python的学习,从此开始慢慢熬夜,也渐渐地不怎么出去骑车了。
2019 总的来说,还比较满意吧,主要是感觉过得很充实,大三基本上每天一整天都是上机课,没有太多时间搞自己的,自己倾向于Python、网络爬虫、数据分析方面,然而这些课程学校都没有,每天晚上以及周六周日都是自己在学,找了不少视频在看,有时候感觉自己还是差点火候,感觉一个简单的东西人家看一遍就会,但是我要看好几遍,不管怎样,我还是相信勤能补拙的。
暑假受家族前辈的邀请,为整个姓氏家族编写族谱,感觉这是今年收获最大的一件事情吧,当时背着电脑跟着前辈下乡,挨家挨户统计资料,纯手工录入电脑(感觉那是我活了二十年打字打得最多的一个月,祖宗十八代都搞清楚了),最后排版打印成书,一个月下来感受到了信息化时代和传统文化的碰撞,见了很多古书,古迹,当然还领略到了古繁体字的魅力,前辈一路上给我讲述了很多书本上学不到的东西,一段很有意义的体验,感触颇深。
个人爱好上面,今年就基本上没有骑车了,没有经常骑车,开学骑了两次就跟不上别人了,后面就洗干净用布遮起来放在寝室了,按照目前情况来看,多半是要“退役”了,不知道何时才会又一次踩上脚踏,不过偶尔还是在抖音上刷刷关注的单车大佬,看看别人的视频,看到友链小伙伴 Shan San 在今年总结也写了他一年没有跳舞了,抛弃了曾经热爱的 Breaking,真的是深有感触啊。
有个遗憾就是大一的愿望实现不了了,恐怕大学四年也不会去西藏或者青海湖了,此处放一个到目前为止的骑行数据,以此纪念一下我的单车生涯吧。
自从搭建了博客之后,认识了不少大佬,经常会去大佬博客逛逛,涨涨知识
截止目前,个人博客 PV:4万+,UV:1万+,知乎:400+赞同,CSDN:43万+访问量,400+赞同
此外今年第一次为开源做了一点儿微不足道的贡献,为 Hexo 博客主题 Material X 添加了文章字数统计和阅读时长的功能,提交了人生当中第一个 PR。第一次嘛,还是值得纪念一下的。
我 GitHub 上虽然有一些小绿点,但是很大一部分都是推送的博客相关的东西,剩下的有几个仓库也就是 Python 相关的了,一些实战的代码放在了上面,很多时候是拿 GitHub 围观一些牛逼代码或者资源,还需要努力学习啊!
实战方面,爬虫自己也爬了很多网站,遇到一些反爬网站还不能解决,也刷了一些 Checkio 上面的题,做了题,和其他大佬相比才会发现自己的代码水平有多低,最直接的感受就是我用了很多行代码,而大神一行代码就解决了,只能说自己的水平还有很大的增进空间,新的一年继续努力吧!
1024 + 996 = 2020,2020注定是不平凡的一年,定下目标,努力实现,只谈技术,莫问前程!
1 | 2019 pip uninstall |
爬取时间:2019-11-14
爬取难度:★★☆☆☆☆
请求链接:https://www.guazi.com/www/buy/
爬取目标:爬取瓜子全国二手车信息,包括价格、上牌时间、表显里程等;保存车辆图片
涉及知识:请求库 requests、解析库 lxml、Xpath 语法、数据库 MongoDB 的操作
完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/guazi
其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice
爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278
分析页面,按照习惯,最开始在 headers 里面只加入 User-Agent 字段,向主页发送请求,然而返回的东西并不是主页真正的源码,因此我们加入 Cookie,再次发起请求,即可得到真实数据。
获取 Cookie:打开浏览器访问网站,打开开发工具,切换到 Network 选项卡,筛选 Doc 文件,在 Request Headers 里可以看到 Cookie 值。
注意在爬取瓜子二手车的时候,User-Agent 与 Cookie 要对应一致,也就是直接复制 Request Headers 里的 User-Agent 和 Cookie,不要自己定义一个 User-Agent,不然有可能获取不到信息!分析页面,请求地址为:https://www.guazi.com/www/buy/
第一页:https://www.guazi.com/www/buy/
第二页:https://www.guazi.com/www/buy/o2c-1/
第三页:https://www.guazi.com/www/buy/o3c-1/
一共有50页数据,利用 for 循环,每次改变 URL 中 o2c-1
参数里面的数字即可实现所有页面的爬取,由于我们是想爬取每台二手车详情页的数据,所以定义一个 parse_index()
函数,提取每一页的所有详情页的 URL,保存在列表 url_list
中
1 | # 必须要有 Cookie 和 User-Agent,且两者必须对应(用浏览器访问网站后控制台里面复制) |
前面的第一步我们已经获取到了二手车详情页的 URL,现在定义一个 parse_detail()
函数,向其中循环传入每一条 URL,利用 Xpath 语法匹配每一条信息,所有信息包含:标题、二手车价格、新车指导价、车主、上牌时间、表显里程、上牌地、排放标准、变速箱、排量、过户次数、看车地点、年检到期、交强险、商业险到期。
其中有部分信息可能包含空格,可以用 strip() 方法将其去掉。
需要注意的是,上牌地对应的是一个 class="three"
的 li
标签,有些二手车没有上牌地信息,匹配的结果将是空,在数据储存时就有可能出现数组越界的错误信息,所以这里可以加一个判断,如果没有上牌地信息,可以将其赋值为:未知。
保存车辆图片时,为了节省时间和空间,避免频繁爬取被封,所以只保存第一张图片,同样利用 Xpath 匹配到第一张图片的地址,以标题为图片的名称,定义储存路径后,以二进制形式保存图片。
最后整个函数返回的是一个列表 data
,这个列表包含每辆二手车的所有信息
1 | # 获取二手车详细信息 |
定义数据储存函数 save_data()
使用 MongoClient()
方法,向其传入地址参数 host
和 端口参数 port
,指定数据库为 guazi
,集合为 esc
传入第二步 parse_detail()
函数返回的二手车信息的列表,依次读取其中的元素,每一个元素对应相应的信息名称
最后调用 insert_one()
方法,每次插入一辆二手车的数据
1 | # 将数据储存到 MongoDB |
1 | # ============================================= |
爬取的汽车图片:
储存到 MongoDB 的数据:
数据导出为 CSV 文件:
Cookie 过一段时间就会失效,数据还没爬取完就失效了,导致无法继续爬取;爬取效率不高,可以考虑多线程爬取
]]>爬取时间:2019-10-21
爬取难度:★★★☆☆☆
请求链接:https://wh.58.com/chuzu/
爬取目标:58同城武汉出租房的所有信息
涉及知识:网站加密字体的攻克、请求库 requests、解析库 Beautiful Soup、数据库 MySQL 的操作
完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/58tongcheng
其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice
爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278
F12 打开调试模板,通过页面分析,可以观察到,网站里面凡是涉及到有数字的地方,都是显示为乱码,这种情况就是字体加密了,那么是通过什么手段实现字体加密的呢?
CSS 中有一个 @font-face
规则,它允许为网页指定在线字体,也就是说可以引入自定义字体,这个规则本意是用来消除对电脑字体的依赖,现在不少网站也利用这个规则来实现反爬
右侧可以看到网站用的字体,其他的都是常见的微软雅黑,宋体等,但是有一个特殊的:fangchan-secret
,不难看出这应该就是58同城的自定义字体了
我们通过控制台看到的乱码事实上是由于 unicode 编码导致,查看网页源代码,我们才能看到他真正的编码信息
要攻克加密字体,那么我们肯定要分析他的字体文件了,先想办法得到他的加密字体文件,同样查看源代码,在源代码中搜索 fangchan-secret
的字体信息
选中的蓝色部分就是 base64 编码的加密字体字符串了,我们将其解码成二进制编码,写进 .woff
的字体文件,这个过程可以通过以下代码实现:
1 | import requests |
得到字体文件后,我们可以通过 FontCreator 这个软件来看看字体对应的编码是什么:
观察我们在网页源代码中看到的编码:类似于 龤
、龒
对比字体文件对应的编码:类似于 uni9FA4
、nui9F92
可以看到除了前面三个字符不一样以外,后面的字符都是一样的,只不过英文大小写有所差异
现在我们可能会想到,直接把编码替换成对应的数字不就OK了?然而并没有这么简单
尝试刷新一下网页,可以观察到 base64 编码的加密字体字符串会改变,也就是说编码和数字并不是一一对应的,再次获取几个字体文件,通过对比就可以看出来
可以看到,虽然每次数字对应的编码都不一样,但是编码总是这10个,是不变的,那么编码与数字之间肯定存在某种对应关系,,我们可以将字体文件转换为 xml 文件来观察其中的对应关系,改进原来的代码即可实现转换功能:
1 | import requests |
打开 58font.xml
文件并分析,在 <cmap>
标签内可以看到熟悉的类似于 0x9476
、0x958f
的编码,其后四位字符恰好是网页字体的加密编码,可以看到每一个编码后面都对应了一个 glyph
开头的编码
将其与 58font.woff
文件对比,可以看到 code 为 0x958f
这个编码对应的是数字 3
,对应的 name 编码是 glyph00004
我们再次获取一个字体文件作为对比分析
依然是 0x958f
这个编码,两次对应的 name 分别是 glyph00004
和 glyph00007
,两次对应的数字分别是 3
和 6
,那么结论就来了,每次发送请求,code 对应的 name 会随机发生变化,而 name 对应的数字不会发生变化,glyph00001
对应数字 0
、glyph00002
对应数字 1
,以此类推
那么以 glyph
开头的编码是如何对应相应的数字的呢?在 xml 文件里面,每个编码都有一个 TTGlyph
的标签,标签里面是一行一行的类似于 x,y 坐标的东西,这个其实就是用来绘制字体的,用 matplotlib 根据坐标画个图,就可以看到是一个数字
此时,我们就知道了编码与数字的对应关系,下一步,我们可以查找 xml 文件里,编码对应的 name 的值,也就是以 glyph
开头的编码,然后返回其对应的数字,再替换掉网页源代码里的编码,就能成功获取到我们需要的信息了!
总结一下攻克加密字体的大致思路:
分析网页,找到对应的加密字体文件
如果引用的加密字体是一个 base64 编码的字符串,则需要转换成二进制并保存到 woff 字体文件中
将字体文件转换成 xml 文件
用 FontCreator 软件观察字体文件,结合 xml 文件,分析其编码与真实字体的关系
搞清楚编码与字体的关系后,想办法将编码替换成正常字体
1 | def get_font(page_url, page_num): |
由主函数传入要发送请求的 url,利用字符串的 split()
方法,匹配 base64 编码的加密字体字符串,利用 base64
模块的 base64.decodebytes()
方法,将 base64 编码的字体字符串解码成二进制编码并保存为字体文件,利用 FontTools
库,将字体文件转换为 xml 文件
1 | def find_font(): |
由前面的分析,我们知道 name 的值(即以 glyph 开头的编码)对应的数字是固定的,glyph00001
对应数字 0
、glyph00002
对应数字 1
,以此类推,所以可以将其构造成为一个字典 glyph_list
同样将十个 code(即类似于 0x9476
的加密字体编码)构造成一个列表
循环查找这十个 code
在 xml 文件里对应的 name
的值,然后将 name
的值与字典文件的 key
值进行对比,如果两者值相同,则获取这个 key
的 value
值,最终得到的列表 num_list
,里面的元素就是 unicode_list
列表里面每个加密字体的真实值
1 | def replace_font(num, page_response): |
传入由上一步 find_font()
函数得到的真实字体的列表,利用 replace()
方法,依次将十个加密字体编码替换掉
1 | def parse_pages(pages): |
利用 BeautifulSoup 解析库很容易提取到相关信息,这里要注意的是,租房信息来源分为三种:经纪人、品牌公寓和个人房源,这三个的元素节点也不一样,因此匹配的时候要注意
1 | def create_mysql_table(): |
首先指定数据库为 58tc_spiders,需要事先使用 MySQL 语句创建,也可以通过 MySQL Workbench 手动创建
然后使用 SQL 语句创建 一个表:58tc_data,表中包含 title、price、layout、address、agent 五个字段,类型都为 varchar
此创建表的操作也可以事先手动创建,手动创建后就不需要此函数了
1 | def save_to_mysql(data): |
commit()
方法的作用是实现数据插入,是真正将语句提交到数据库执行的方法,使用 try except
语句实现异常处理,如果执行失败,则调用 rollback()
方法执行数据回滚,保证原数据不被破坏
1 | # ============================================= |
登陆时间:2019-10-21
实现难度:★★★☆☆☆
请求链接:https://kyfw.12306.cn/otn/resources/login.html
实现目标:模拟登陆中国铁路12306,攻克点触验证码
涉及知识:点触验证码的攻克、自动化测试工具 Selenium 的使用、对接在线打码平台
完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/12306-login
其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice
爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278
利用自动化测试工具 Selenium 直接模拟人的行为方式来完成验证
发送请求,出现验证码后,剪裁并保存验证码图片
选择在线打码平台,获取其API,以字节流格式发送图片
打码平台人工识别验证码,返回验证码的坐标信息
解析返回的坐标信息,模拟点击验证码,完成验证后点击登陆
关于打码平台:在线打码平台全部都是人工在线识别,准确率非常高,原理就是先将验证码图片提交给平台,平台会返回识别结果在图片中的坐标位置,然后我们再解析坐标模拟点击即可,常见的打码平台有超级鹰、云打码等,打码平台是收费的,拿超级鹰来说,1元 = 1000题分,识别一次验证码将花费一定的题分,不同类型验证码需要的题分不同,验证码越复杂所需题分越高,比如 7 位中文汉字需要 70 题分,常见 4 ~ 6 位英文数字只要 10 题分,其他打码平台价格也都差不多,本次实战使用超级鹰打码平台
使用打码平台:在超级鹰打码平台注册账号,官网:http://www.chaojiying.com/ ,充值一块钱得到 1000 题分,在用户中心里面申请一个软件 ID ,在价格体系里面确定验证码的类型,先观察 12306 官网,发现验证码是要我们点击所有满足条件的图片,一般有 1 至 4 张图片满足要求,由此可确定在超级鹰打码平台的验证码类型为 9004(坐标多选,返回1~4个坐标,如:x1,y1|x2,y2|x3,y3), 然后在开发文档里面获取其 Python API,下载下来以备后用
1 | # 12306账号密码 |
定义 12306 账号(USERNAME
)、密码(PASSWORD
)、超级鹰用户名(CHAOJIYING_USERNAME
)、超级鹰登录密码(CHAOJIYING_PASSWORD
)、超级鹰软件 ID(CHAOJIYING_SOFT_ID
)、验证码类型(CHAOJIYING_KIND
),登录页面 url ,谷歌浏览器驱动的目录(path
),浏览器启动参数等,将超级鹰账号密码等相关参数传递给超级鹰 API
1 | def get_input_element(self): |
分析页面可知,登陆页面默认出现的是扫描二维码登陆,所以要先点击账号登录,找到该 CSS 元素为 login-hd-account
,调用 click()
方法实现模拟点击,此时出现账号密码输入框,同样找到其 ID 分别为 J-userName
和 J-password
,调用 send_keys()
方法输入账号密码
1 | def crack(self): |
crack()
为验证码处理模块的主函数
调用账号密码输入函数 get_input_element()
,等待账号密码输入完毕
调用验证码图片剪裁函数 get_touclick_image()
,得到验证码图片
利用超级鹰打码平台的 API PostPic()
方法把图片发送给超级鹰后台,发送的图像是字节流格式,返回的结果是一个JSON,如果识别成功,典型的返回结果类似于:
1 | {'err_no': 0, 'err_str': 'OK', 'pic_id': '6002001380949200001', 'pic_str': '132,127|56,77', 'md5': |
其中,pic_str
就是识别的文字的坐标,是以字符串形式返回的,每个坐标都以 |
分隔
调用 get_points()
函数解析超级鹰识别结果
调用 touch_click_words()
函数对符合要求的图片进行点击
调用模拟点击登录函数 login()
,点击登陆按钮模拟登陆
使用 try-except
语句判断是否出现了用户信息,判断依据是是否有用户姓名的出现,出现的姓名和实际姓名一致则登录成功,如果失败了就重试,超级鹰会返回该分值
1 | def get_touclick_image(self, name='12306.png'): |
首先查找到验证码的坐标信息,先对整个页面截图,然后根据验证码坐标信息,剪裁出验证码图片
location 属性可以返回该图片对象在浏览器中的位置,坐标轴是以屏幕左上角为原点,x 轴向右递增,y 轴向下递增,size 属性可以返回该图片对象的高度和宽度,由此可以得到验证码的位置信息
1 | def get_points(self, captcha_result): |
get_points()
方法将超级鹰的验证码识别结果变成列表的形式
1 | def touch_click_words(self, locations): |
循环提取正确的验证码坐标信息,依次点击验证码
1 | def login(self): |
分析页面,找到登陆按钮的 ID 为 J-login
,调用 click()
方法模拟点击按钮实现登录
1 | # ============================================= |
1 | import requests |
最终实现效果图:(关键信息已经过打码处理)
登陆时间:2019-10-21
实现难度:★★★☆☆☆
请求链接:https://passport.bilibili.com/login
实现目标:模拟登陆哔哩哔哩,攻克滑动验证码
涉及知识:滑动验证码的攻克、自动化测试工具 Selenium 的使用
完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/bilibili-login
其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice
爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278
利用自动化测试工具 Selenium 直接模拟人的行为方式来完成验证
分析页面,想办法找到滑动验证码的完整图片、带有缺口的图片和需要滑动的图片
对比原始的图片和带缺口的图片的像素,像素不同的地方就是缺口位置
计算出滑块缺口的位置,得到所需要滑动的距离
拖拽时要模仿人的行为,由于有个对准过程,所以要构造先快后慢的运动轨迹
最后利用 Selenium 进行对滑块的拖拽
1 | def init(): |
global
关键字定义了发起请求的url、用户名、密码等全局变量,随后是登录页面url、谷歌浏览器驱动的目录path、实例化 Chrome 浏览器、设置浏览器分辨率最大化、用户名、密码、WebDriverWait()
方法设置等待超时
1 | def login(): |
等待用户名输入框和密码输入框对应的 ID 节点加载出来
获取这两个节点,用户名输入框 id="login-username"
,密码输入框 id="login-passwd"
调用 send_keys()
方法输入用户名和密码
获取登录按钮 class="btn btn-login"
随机产生一个数并将其扩大三倍作为暂停时间
最后调用 click()
方法实现登录按钮的点击
1 | def find_element(): |
获取验证码的三张图片,分别是完整的图片、带有缺口的图片和需要滑动的图片
分析页面代码,三张图片是由 3 个 canvas 组成,3 个 canvas 元素包含 CSS display
属性,display:block
为可见,display:none
为不可见,在分别获取三张图片时要将其他两张图片设置为 display:none
,这样做才能单独提取到每张图片
定位三张图片的 class 分别为:带有缺口的图片(c_background):geetest_canvas_bg geetest_absolute
、需要滑动的图片(c_slice):geetest_canvas_slice geetest_absolute
、完整图片(c_full_bg):geetest_canvas_fullbg geetest_fade geetest_absolute
最后传值给 save_screenshot()
函数,进一步对验证码进行处理
1 | # 设置元素不可见 |
1 | def save_screenshot(obj, name): |
location
属性可以返回该图片对象在浏览器中的位置,坐标轴是以屏幕左上角为原点,x轴向右递增,y轴向下递增
size
属性可以返回该图片对象的高度和宽度,由此可以得到验证码的位置信息
首先调用 save_screenshot()
属性对整个页面截图并保存
然后向 crop()
方法传入验证码的位置信息,由位置信息再对验证码进行剪裁并保存
1 | def slide(): |
向 get_distance()
函数传入完整的图片和缺口图片,计算滑块需要滑动的距离,再把距离信息传入 get_trace()
函数,构造滑块的移动轨迹,最后根据轨迹信息调用 move_to_gap()
函数移动滑块完成验证
1 | def is_pixel_equal(bg_image, fullbg_image, x, y): |
将完整图片和缺口图片两个对象分别赋值给变量 bg_image
和 fullbg_image
,接下来对比图片获取缺口。遍历图片的每个坐标点,获取两张图片对应像素点的 RGB 数据,判断像素的各个颜色之差,abs()
用于取绝对值,比较两张图 RGB 的绝对值是否均小于定义的阈值 threshold,如果绝对值均在阈值之内,则代表像素点相同,继续遍历,否则代表不相同的像素点,即缺口的位置
1 | def get_distance(bg_image, fullbg_image): |
get_distance()
方法即获取缺口位置的方法,此方法的参数是两张图片,一张为完整的图片,另一张为带缺口的图片,distance
为滑块的初始位置,遍历两张图片的每个像素,利用 is_pixel_equal()
缺口位置寻找函数判断两张图片同一位置的像素是否相同,若不相同则返回该点的值
1 | def get_trace(distance): |
get_trace()
方法传入的参数为移动的总距离,返回的是运动轨迹,运动轨迹用 trace 表示,它是一个列表,列表的每个元素代表每次移动多少距离,利用 Selenium 进行对滑块的拖拽时要模仿人的行为,由于有个对准过程,所以是先快后慢,匀速移动、随机速度移动都不会成功,因此要设置一个加速和减速的距离,这里设置加速距离 faster_distance
是总距离 distance
的4/5倍,滑块滑动的加速度用 a 来表示,当前速度用 v 表示,初速度用 v0 表示,位移用 move 表示,所需时间用 t 表示,它们之间满足以下关系:
1 | move = v0 * t + 0.5 * a * t * t |
设置初始位置、初始速度、时间间隔分别为0, 0, 0.1,加速阶段和减速阶段的加速度分别设置为10和-10,直到运动轨迹达到总距离时,循环终止,最后得到的 trace 记录了每个时间间隔移动了多少位移,这样滑块的运动轨迹就得到了
1 | def move_to_gap(trace): |
传入的参数为运动轨迹,首先查找到滑动按钮,然后调用 ActionChains 的 click_and_hold()
方法按住拖动底部滑块,perform()
方法用于执行,遍历运动轨迹获取每小段位移距离,调用 move_by_offset()
方法移动此位移,最后调用 release()
方法松开鼠标即可
1 | # ============================================= |
最终实现效果图:(关键信息已经过打码处理)
爬取时间:2019-10-12
爬取难度:★★☆☆☆☆
请求链接:https://bbs.hupu.com/bxj
爬取目标:爬取虎扑论坛步行街的帖子,包含主题,作者,发布时间等,数据保存到 MongoDB 数据库
涉及知识:请求库 requests、解析库 Beautiful Soup、数据库 MongoDB 的操作
完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/hupu
其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice
爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278
观察虎扑论坛步行街分区,请求地址为:https://bbs.hupu.com/bxj
第二页:https://bbs.hupu.com/bxj-2
第三页:https://bbs.hupu.com/bxj-3
不难发现,每增加一页,只需要添加 -页数
参数即可,最后一页是第 50 页,因此可以利用 for 循环依次爬取,定义一个 get_pages()
函数,返回初始化 Beautiful Soup 的对象 page_soup,方便后面的解析函数调用
虽然一共有 50 页,但是当用户访问第 10 页以后的页面的时候,会要求登录虎扑,不然就没法查看,而且登录时会出现智能验证,所以程序只爬取前 10 页的数据
1 | def get_pages(page_url): |
使用 Beautiful Soup 对网页各个信息进行提取,最后将这些信息放进一个列表里,然后调用列表的 .append()
方法,再将每条帖子的列表依次加到另一个新列表里,最终返回的是类似于如下形式的列表:
1 | [['帖子1', '作者1'], ['帖子2', '作者2'], ['帖子3', '作者3']] |
这样做的目的是:方便 MongoDB 依次储存每一条帖子的信息
1 | def parse_pages(page_soup): |
首先使用 MongoClient()
方法,向其传入地址参数 host 和 端口参数 port,指定数据库为 hupu
,集合为 bxj
将解析函数返回的列表传入到储存函数,依次循环该列表,对每一条帖子的信息进行提取并储存
1 | def mongodb(data_list): |
1 | # ============================================= |
一共爬取到 1180 条数据:
程序只能爬取前 10 页的数据,因为虎扑论坛要求从第 11 页开始,必须登录账号才能查看,并且登录时会有智能验证,可以使用自动化测试工具 Selenium 模拟登录账号后再进行爬取。
]]>爬取时间:2019-10-09
爬取难度:★★☆☆☆☆
请求链接:https://wuhan.anjuke.com/sale/
爬取目标:爬取武汉二手房每一条售房信息,包含地理位置、价格、面积等,保存为 CSV 文件
涉及知识:请求库 requests、解析库 Beautiful Soup、CSV 文件储存、列表操作、分页判断
完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/anjuke
其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice
爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278
分析 安居客武汉二手房页面,这次爬取实战准备使用 BeautifulSoup 解析库,熟练 BeautifulSoup 解析库的用法,注意到该页面与其他页面不同的是,不能一次性看到到底有多少页,以前知道一共有多少页,直接一个循环爬取就行了,虽然可以通过改变 url 来尝试找到最后一页,但是这样就显得不程序员了😂,因此可以通过 BeautifulSoup 解析 下一页按钮
,提取到下一页的 url,直到没有 下一页按钮
这个元素为止,从而实现所有页面的爬取,剩下的信息提取和储存就比较简单了
分析页面,可以发现每条二手房信息都是包含在 <li>
标签内的,因此可以使用 BeautifulSoup 解析页面得到所有的 <li>
标签,然后再循环访问每个 <li>
标签,依次解析得到每条二手房的各种信息
1 | def parse_pages(url, num): |
前面已经分析过,该网页是无法一下就能看到一共有多少页的,尝试找到最后一页,发现一共有50页,那么此时就可以搞个循环,一直到第50页就行了,但是如果有一天页面数增加了呢,那么代码的可维护性就不好了,我们可以观察 下一页按钮
,当存在下一页的时候,是 <a>
标签,并且带有下一页的 URL,不存在下一页的时候是 <i>
标签,因此可以写个 if
语句,判断是否存在此 <a>
标签,若存在,表示有下一页,然后提取其 href
属性并传给解析模块,实现后面所有页面的信息提取,此外,由于安居客有反爬系统,我们还可以利用 Python中的 random.randint()
方法,在两个数值之间随机取一个数,传入 time.sleep()
方法,实现随机暂停爬取
1 | # 判断是否还有下一页 |
数据储存比较简单,将每个二手房信息组成一个列表,依次写入到 anjuke.csv 文件中即可
1 | results = [title, layout, cover, floor, year, unit_price, total_price, keyword, address, details_url] |
1 | # ============================================= |
虽然使用了随机暂停爬取的方法,但是在爬取了大约 20 页的数据后依然会出现验证页面,导致程序终止
原来设想的是可以由用户手动输入城市的拼音来查询不同城市的信息,方法是把用户输入的城市拼音和其他参数一起构造成一个 URL,然后对该 URL 发送请求,判断请求返回的代码,如果是 200 就代表可以访问,也就是用户输入的城市是正确的,然而发现即便是输入错误,该 URL 依然可以访问,只不过会跳转到一个正确的页面,没有搞清楚是什么原理,也就无法实现由用户输入城市来查询这个功能
欢迎关注我的 CSDN 专栏:《个人博客搭建:Hexo+Github Pages》,从搭建到美化一条龙,帮你解决 Hexo 常见问题!
由于 Hexo 博客是静态托管的,所有的原始数据都保存在本地,如果哪一天电脑坏了,或者是误删了本地数据,那就是叫天天不应叫地地不灵了,此时定时备份就显得比较重要了,常见的备份方法有:打包数据保存到U盘、云盘或者其他地方,但是早就有大神开发了备份插件:hexo-git-backup ,只需要一个命令就可以将所有数据包括主题文件备份到 github 了
首先进入你博客目录,输入命令 hexo version
查看 Hexo 版本,如图所示,我的版本是 3.7.1:
安装备份插件,如果你的 Hexo 版本是 2.x.x,则使用以下命令安装:
1 | $ npm install hexo-git-backup@0.0.91 --save |
如果你的 Hexo 版本是 3.x.x,则使用以下命令安装:
1 | $ npm install hexo-git-backup --save |
到 Hexo 博客根目录的 _config.yml
配置文件里添加以下配置:
1 | backup: |
参数解释:
最后使用以下命令备份你的博客:
1 | $ hexo backup |
或者使用以下简写命令也可以:
1 | $ hexo b |
备份成功后可以在你的仓库分支下看到备份的原始文件:
爬取时间:2019-09-27
爬取难度:★★☆☆☆☆
请求链接:https://movie.douban.com/top250 以及每部电影详情页
爬取目标:爬取榜单上每一部电影详情页的数据,保存为 CSV 文件;下载所有电影海报到本地
涉及知识:请求库 requests、解析库 lxml、Xpath 语法、正则表达式、CSV 和二进制数据储存、列表操作
完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/douban-top250
其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice
爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278
观察豆瓣电影 Top 250,请求地址为:https://movie.douban.com/top250
每页展示25条电影信息,照例翻页观察 url 的变化:
第一页:https://movie.douban.com/top250
第二页:https://movie.douban.com/top250?start=25&filter=
第三页:https://movie.douban.com/top250?start=50&filter=
一共有10页,每次改变的是 start 的值,利用一个 for 循环,从 0 到 250 每隔 25 取一个值拼接到 url,实现循环爬取每一页,由于我们的目标是进入每一部电影的详情页,然后爬取详情页的内容,所以我们可以使用 Xpath 提取每一页每部电影详情页的 URL,将其赋值给 m_urls
,并返回 m_urls
,m_urls
是一个列表,列表元素就是电影详情页的 URL
1 | def index_pages(number): |
定义一个解析函数 parse_pages()
,利用 for 循环,依次提取 index_pages()
函数返回的列表中的元素,也就是每部电影详情页的 URL,将其传给解析函数进行解析
1 | def index_pages(number): |
详细看一下解析函数 parse_pages()
,首先要对接收到的详情页 URL 发送请求,获取响应内容,然后再使用 Xpath 提取相关信息
1 | def parse_pages(url): |
其中排名、电影名和评分信息是最容易匹配到的,直接使用 Xpath 语法就可以轻松解决:
1 | # 排名 |
接下来准备爬取有多少人参与了评价,分析一下页面:
如果只爬取这个 <span>
标签下的数字的话,没有任何提示信息,别人看了不知道是啥东西,所以把 人评价
这三个字也爬下来的话就比较好了,但是可以看到数字和文字不在同一个元素标签下,而且文字部分还有空格,要爬取的话就要把 class="rating_people"
的 a
标签下所有的 text
提取出来,然后再去掉空格:
1 | # 参评人数 |
这样做太麻烦了,我们可以直接提取数字,得到一个列表,然后使用另一个带有提示信息的列表,将两个列表的元素合并,组成一个新列表,这个新列表的元素就是提示信息+人数1
2
3
4
5
6# 参评人数
value = parse_movie.xpath("//span[@property='v:votes']/text()")
# 合并元素
number = [" ".join(['参评人数:'] + value)]
# 此时 number = ['参评人数:1617307']
接下来尝试爬取制片国家/地区、语言等信息:
分析页面可以观察到,制片国家/地区和语言结构比较特殊,没有特别的 class 或者 id 属性,所包含的层次关系也太复杂,所以这里为了简便,直接采用正则表达式来匹配信息,就没有那么复杂了:
1 | # 制片国家/地区 |
其他剩下的信息皆可利用以上方法进行提取,所有信息提取完毕,最后使用 zip()
函数,将所有提取的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表
1 | return zip(ranking, name, score, number, types, country, language, date, time, other_name, director, screenwriter, performer, m_url, imdb_url) |
定义一个数据保存函数 save_results()
1 | def save_results(data): |
注意:编码方式要设置为 utf-8-sig
,如果设置为 utf-8
,则文件会乱码,不设置编码,则可能会报一下类似错误:
1 | UnicodeEncodeError: 'gbk' codec can't encode character '\ub3c4' in position 9: illegal multibyte sequence |
可以看到错误出现在 \ub3c4
上,将该 Unicode 编码转换为中文为 도
,发现正是排名第 19 的电影:熔炉 도가니,因为标题有韩文,所以在储存为 CSV 文件时会报编码错误,而将编码设置为 utf-8-sig
就不会报错,具体原因参见:《Python 中文日文汉字乱码处理utf-8-sig》
接下来是保存电影的海报到本地:
1 | # 保存电影海报 |
解析电影详情页,使用 Xpath 提取海报的 URL,向该 URL 发送请求
图片以 排名+电影名.jpg
的方式命名,但是由于提取的电影名部分含有特殊字符,比如排名第 10 的电影:忠犬八公的故事 Hachi: A Dog’s Tale,其中有个冒号,而 Windows 文件命名是不能包含这些字符的,所以我们直接去除电影名包含的英文字符、空白字符、特殊字符,只留下中文,代码实现: name2 = re.sub(r'[A-Za-z\:\s]', '', name[0])
定义一个文件夹名称 douban_poster
,利用 os
模块判断当前是否存在该文件夹,若不存在就创建一个
最后以二进制形式保存海报到当前目录的 douban_poster 文件夹下
1 | # ============================================= |
程序不足的地方:豆瓣电影有反爬机制,当程序爬取到大约 150 条数据的时候,IP 就会被封掉,第二天 IP 才会解封,可以考虑综合使用多个代理、多个 User-Agent、随机时间暂停等方法进行爬取
]]>爬取时间:2019-09-23
爬取难度:★☆☆☆☆☆
请求链接:https://maoyan.com/board/4
爬取目标:猫眼 TOP100 的电影名称、排名、主演、上映时间、评分、封面图地址,数据保存为 CSV 文件
涉及知识:请求库 requests、解析库 lxml、Xpath 语法、CSV 文件储存
完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/maoyan-top100
其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice
爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278
观察猫眼电影TOP100榜,请求地址为:https://maoyan.com/board/4
每页展示10条电影信息,翻页观察 url 变化:
第一页:https://maoyan.com/board/4
第二页:https://maoyan.com/board/4?offset=10
第三页:https://maoyan.com/board/4?offset=20
一共有10页,利用一个 for 循环,从 0 到 100 每隔 10 取一个值拼接到 url,实现循环爬取每一页
1 | def index_page(number): |
定义一个页面解析函数 parse_page()
,使用 lxml 解析库的 Xpath 方法依次提取电影排名(ranking)、电影名称(movie_name)、主演(performer)、上映时间(releasetime)、评分(score)、电影封面图 url(movie_img)
通过对主演部分的提取发现有多余的空格符和换行符,循环 performer 列表,使用 strip()
方法去除字符串头尾空格和换行符
电影评分分为整数部分和小数部分,依次提取两部分,循环遍历组成一个完整的评分
最后使用 zip()
函数,将所有提取的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表
1 | def parse_page(content): |
定义一个 save_results()
函数,将所有数据保存到 maoyan.csv
文件
1 | def save_results(result): |
1 | # ============================================= |
Python3 爬虫学习笔记第十八章 —— 【爬虫框架 pyspider — 深入理解】
常用启动命令:pyspider all
,完整命令结构为:pyspider [OPTIONS] COMMAND [ARGS]
,OPTIONS 为可选参数,包含以下参数:
配置文件为一个 JSON 文件,一般为 config.json 文件,常用配置如下:
1 | { |
可以设置对应的用户名,密码,端口等信息,使用命令 pyspider -c config.json all
即可运行
pyspider 的架构主要分为 Scheduler(调度器)、Fetcher(抓取器)、Processer(处理器)三个部分,都可以单独运行,基本命令: pyspider [component_name] [options]
1 | pyspider scheduler [OPTIONS] |
1 | Options: |
1 | pyspider fetcher [OPTIONS] |
1 | Options: |
1 | pyspider processor [OPTIONS] |
1 | Options: |
1 | pyspider webui [OPTIONS] |
1 | Options: |
参数文档:http://docs.pyspider.org/en/latest/apis/self.crawl/
1 | def on_start(self): |
代码解释:指定 callback
为 index_page
,代表爬取 http://www.itrhx.com/ 得到的响应会用 index_page()
方法来解析,而 index_page()
方法的第一个参数就是响应对象,如下所示:
1 | def index_page(self, response): |
1 | def on_start(self): |
1 |
|
2.html
页面将会优先爬取:1 | def index_page(self): |
1 | import time |
1 | def index_page(self, response): |
代码解释:设置 update-time
这个节点的值为 itag,在下次爬取时就会首先检测这个值有没有发生变化,如果没有变化,则不再重复爬取,否则执行爬取
1 | def on_start(self): |
代码解释:定义 age
有效期为 5 小时,设置了 auto_recrawl
为 True
,这样任务就会每 5 小时执行一次
1 | def on_start(self): |
1 | def on_start(self): |
1 | def on_start(self): |
username:password@hostname:port
,如下所示:1 | def on_start(self): |
也可以设置 craw_config
来实现全局配置,如下所示:
1 | class Handler(BaseHandler): |
1 | def on_start(self): |
1 | def on_start(self): |
document-end
1 | def on_start(self): |
ACTIVE
状态的,则需要将 force_update
设置为 True
pyspider 判断两个任务是否是重复的是使用的是该任务对应的 URL 的 MD5 值作为任务的唯一 ID,如果 ID 相同,那么两个任务就会判定为相同,其中一个就不会爬取了
某些情况下,请求的链接是同一个,但是 POST 的参数不同,这时可以重写 task_id()
方法,利用 URL 和 POST 的参数来生成 ID,改变这个 ID 的计算方式来实现不同任务的区分:
1 | import json |
pyspider 可以使用 crawl_config
来指定全局的配置,配置中的参数会和 crawl()
方法创建任务时的参数合并:
1 | class Handler(BaseHandler): |
通过 every
属性来设置爬取的时间间隔,如下代码表示每天执行一次爬取:
1 |
|
注意事项:如果设置了任务的有效时间(age 参数),因为在有效时间内爬取不会重复,所以要把有效时间设置得比重复时间更短,这样才可以实现定时爬取
错误举例:设定任务的过期时间为 5 天,而自动爬取的时间间隔为 1 天,当第二次尝试重新爬取的时候,pyspider 会监测到此任务尚未过期,便不会执行爬取:
1 |
|
Python3 爬虫学习笔记第十七章 —— 【爬虫框架 pyspider — 基本使用】
pyspider 是由国人 Binux 编写的一个 Python 爬虫框架
pyspider 特性:
Windows 系统安装 pyspider:
使用命令 pip install pyspider
安装,若报 PyCurl 相关错误,可访问 https://www.lfd.uci.edu/~gohlke/pythonlibs/#pycurl 下载对应 wheel 文件并使用命令 pip install whl文件名
安装即可
如果要爬取 JavaScrip 渲染的页面,还要下载 PhantomJS,并将 PhantomJS 的路径配置到环境变量里,或者直接复制到 Python 安装目录的 Scripts 文件夹,需要用到数据库储存的话,同样要安装好相应的数据库
准备就绪后,使用 pyspider all
命令可启动 pyspider,浏览器打开:http://localhost:5000/ 可以看到 pyspider 的 WebUI 管理界面
当成功创建了一个爬虫项目后,主界面如下所示:
Recent Active Tasks:查看最近活动的任务,会跳转到一个页面有列表显示
Create:创建一个新的爬虫项目
group:定义项目的分组,以方便管理,若 group 设置为 delete,则该项目将会在24小时之后删除
project name:爬虫项目名称
status:项目状态,各状态如下:
TODO:一个爬虫项目刚刚创建时的状态,此状态下可以编辑 Python 代码
STOP:中止项目的运行
CHECKING:当一个运行中的项目被编辑时项目状态会被自动设置成此状态并中止运行
DEBUG:会运行爬虫,顾名思义找 BUG,一般来说用于调试阶段
RUNNING:运行爬虫项目
PAUSED:项目暂停运行,默认没有这个状态,但是当你在运行过程中突然断网就会出现此状态
rate/burst:当前的爬取速率,rate 代表 1 秒发出多少个请求,burst 相当于流量控制中的令牌桶算法的令牌数,rate 和 burst 设置的越大,爬取速率越快,速率的设定需要考虑本机性能和爬取过快被封的问题
avg time:任务平均时间
process:5m、1h、1d 分别指的是最近 5 分、1 小时、1 天内的请求情况,all 代表所有的请求情况,请求由不同颜色表示,蓝色的代表等待被执行的请求,绿色的代表成功的请求,黄色的代表请求失败后等待重试的请求,红色的代表失败次数过多而被忽略的请求
actions:对爬虫项目的操作,各操作如下:
Run:立即执行任务,需要 status 为 RUNNING 或者 DEBUG 状态;假如在配置的调度执行时间内已经执行过,再点 run 是无效的,需要删除 task.db 里的数据才行
Active Tasks:查看当前爬虫项目的活动任务
Results:查看项目运行结果
创建一个爬虫项目,界面如下所示:
项目创建完成进入调试界面:
调试界面右边:编写代码的区域
调试界面左边:调试的区域,用于执行代码,显示输出信息等用途
run:单步调试爬虫程序,点击就可运行当前任务
< > 箭头:上一步、下一步,用于调试过程中切换到上一步骤或者下一步骤
save:保存当前代码,当代码变更后只有保存了再运行才能得到最新结果
enable css selector helper: CSS 选择器辅助程序
web:页面预览
html:可以查看页面源代码
follows:表示爬取请求,点击可查看所有的请求
在新建一个爬虫项目的时候,pyspider 已经自动生成了如下代码:
1 | #!/usr/bin/env python |
class Handler():pyspider 爬虫的主类,可以在此处定义爬取、解析、存储的逻辑。整个爬虫的功能只需要一个 Handler 即可完成
crawl_config 属性:项目的所有爬取配置将会统一定义到这里,如定义 headers、设置代理等,配置之后全局生效
on_start() 方法:爬取入口,初始的爬取请求会在这里产生,该方法通过调用 crawl()
方法即可新建一个爬取请求,第一个参数是爬取的 URL,另一个参数 callback
指定了这个页面爬取成功后用哪个方法进行解析,默认指定为 index_page()
方法,即如果这个 URL 对应的页面爬取成功了,那 Response 将交给 index_page()
方法解析
index_page() 方法:接收 Response 参数,Response 对接了 pyquery。直接调用 doc()
方法传入相应的 CSS 选择器,就可以像 pyquery 一样解析此页面,代码中默认是 a[href^="http"]
,即解析页面的所有链接,然后将链接遍历,再次调用了 crawl()
方法生成了新的爬取请求,同时再指定了 callback 为 detail_page,表示这些页面爬取成功了就调用 detail_page()
方法解析。index_page()
实现了两个功能,一是将爬取的结果进行解析,二是生成新的爬取请求
detail_page() 方法:同样接收 Response 作为参数。detail_page()
抓取的就是详情页的信息,就不会生成新的请求,只对 Response 对象做解析,解析之后将结果以字典的形式返回。当然也可以进行后续处理,如将结果保存到数据库等操作
PS:pyspider 默认的 web 预览页面窗口较小,可以找到 pyspider 文件夹有个 debug.min.css 文件(如:E:\Python\Lib\site-packages\pyspider\webui\static\debug.min.css),搜索 iframe,将原样式:iframe{border-width:0;width:100%}
改为 iframe{border-width:0;width:100%;height:400px !important}
即可,清除浏览器缓存后就会生效!
爬取地址:http://travel.qunar.com/travelbook/list.htm
爬取目标:去哪儿网旅游攻略,发帖作者、标题、正文等
创建一个名为 qunar 的爬虫项目,Start URL 设置为 http://travel.qunar.com/travelbook/list.htm ,点击 run 出现一个爬取请求
左边调试区域出现以下代码:
1 | { |
callback 为 on_start,表示此时执行了 on_start()
方法。在 on_start()
方法中,利用 crawl()
方法即可生成一个爬取请求,点击 index_page 链接后面的箭头会出现许多新的爬取请求,即首页所包含的所有链接
此时左边调试区域代码变为:
1 | { |
callback 变为了 index_page,表示此时执行了 index_page()
方法。传入 index_page()
方法的 response 参数为刚才生成的第一个爬取请求的 response 对象,然后调用 doc()
方法,传入提取所有 a 节点的 CSS 选择器,获取 a 节点的属性 href,实现了页面所有链接的提取,随后遍历所有链接,调用 crawl()
方法,把每个链接构造成新的爬取请求,可以看到 follows 新生成了 229 个爬取请求。点击 web 按钮可以直接预览当前页面,点击 html 按钮可以查看此页面源代码
代码 for each in response.doc('a[href^="http"]').items():
实现了对整个页面链接的获取,我们需要提取网页的攻略的标题,内容等信息,那么直接替换 doc()
方法里的匹配语句即可,pyspider 提供了非常方便的 CSS 选择器,点击 enable css selector helper
按钮后,选择要匹配的信息并点击,再点击箭头 add to editor 即可得到匹配语句
完成了 CSS 选择器的替换,点击 save
保存,再次点击 run
重新执行 index_page()
方法,可以看到 follows 变为了 10 个,即抓取到了 10 篇攻略
每一页只有 10 篇攻略,想要爬取所有页面的攻略,必须要得到下一页的数据,优化 index_page()
方法:
1 |
|
匹配下一页按钮,获取下一页按钮的 URL 并赋值给 next,将该 URL 传给 crawl()
方法,指定回调函数为 index_page()
方法,这样会再次调用 index_page()
方法,提取下一页的攻略标题
随便点击一个获取到的攻略,预览该页面,可以观察到头图一直在加载中,切换到 html 查看源代码页面,可以观察到没有 img 节点,那么此处就是后期经过 JavaScript 渲染后才出现的
针对 JavaScript 渲染页面,可以通过 PhantomJS 来实现,具体到 pyspider 中,只需要在 index_page()
的 crawl()
抓取方法中添加一个参数 fetch_type
即可:
1 |
|
保存之后再次运行即可看到正常页面
改写 detail_page()
方法,同样通过 CSS 选择器提取 URL、标题、日期、作者、正文、图片等信息:
1 |
|
该爬虫项目完整代码如下:
1 | #!/usr/bin/env python |
保存代码后,回到主界面,将项目 status 修改为 RUNNING ,点击 actions 的 run 按钮即可启动爬虫
点击 Active Tasks,即可查看最近请求的详细状况:
点击 Results,即可查看所有的爬取结果:
另外,右上角还可以选择 JSON、CSV 格式
]]>网站在没有提交搜索引擎收录之前,直接搜索你网站的内容是搜不到的,只有提交搜索引擎之后,搜索引擎才能收录你的站点,通过爬虫抓取你网站的东西,对于 hexo 博客来说,如果你是部署在 GitHub Pages,那么你是无法被百度收录的,因为 GitHub 禁止了百度爬虫,最常见的解决办法是双线部署到 Coding Pages 和 GitHub Pages,因为百度爬虫可以爬取到 Coding 上的内容,从而实现百度收录,如果你的 hexo 博客还没有实现双线部署,请参考:《Hexo 双线部署到 Coding Pages 和 GitHub Pages 并实现全站 HPPTS》,另外百度收录的所需的时间较长,大约半个月左右才会看到效果!
首先我们可以输入 site:域名
来查看域名是否被搜索引擎收录,如下图所示,表示没有收录:
访问百度搜索资源平台官网,注册或者登陆百度账号,依次选择【用户中心】-【站点管理】,添加你的网站,在添加站点时会让你选择协议头(http 或者 https),如果选择 https,它会验证你的站点,大约能在一天之内完成,我的网站已经实现了全站 https,因此选择了 https 协议,但是不知道为什么始终验证失败,实在是无解,只能选择 http 协议了,如果你的站点也实现了全站 https,也可以尝试一下
之后会让你验证网站所有权,提供三种验证方式:
<head>
与 </head>
标签之间即可完成验证百度提供了自动提交和手动提交两种方式,其中自动提交又分为主动推送、自动推送和 sitemap 三种方式,以下是官方给出的解释:
主动推送:最为快速的提交方式,推荐您将站点当天新产出链接立即通过此方式推送给百度,以保证新链接可以及时被百度收录
自动推送:是轻量级链接提交组件,将自动推送的 JS 代码放置在站点每一个页面源代码中,当页面被访问时,页面链接会自动推送给百度,有利于新页面更快被百度发现
sitemap:您可以定期将网站链接放到sitemap中,然后将sitemap提交给百度。百度会周期性的抓取检查您提交的sitemap,对其中的链接进行处理,但收录速度慢于主动推送
手动提交:如果您不想通过程序提交,那么可以采用此种方式,手动将链接提交给百度
四种提交方式对比:
方式 | 主动推送 | 自动推送 | Sitemap | 手动提交 |
---|---|---|---|---|
速度 | 最快 | —— | —— | —— |
开发成本 | 高 | 低 | 中 | 不需开发 |
可提交量 | 低 | 高 | 高 | 低 |
是否建议提交历史连接 | 否 | 是 | 是 | 是 |
和其他提交方法是否有冲突 | 无 | 无 | 无 | 无 |
个人推荐同时使用主动推送和 sitemap 方式,下面将逐一介绍这四种提交方式的具体实现方法
在博客根目录安装插件 npm install hexo-baidu-url-submit --save
,然后在根目录 _config.yml
文件里写入以下配置:
1 | baidu_url_submit: |
其中的 token
可以在【链接提交】-【自动提交】-【主动推送】下面看到,接口调用地址最后面 token=xxxxx
即为你的 token
同样是在根目录的 _config.yml
文件,大约第 17 行处,url 要改为在百度站长平台添加的域名,也就是你网站的首页地址:
1 | # URL |
最后,加入新的 deployer:
1 | # Deployment |
最后执行 hexo g -d
部署一遍即可实现主动推送,推送成功的标志是:在执行部署命令最后会显示类似如下代码:
1 | {"remain":4999953,"success":47} |
这表示有 47 个页面已经主动推送成功,remain 的意思是当天剩余的可推送 url 条数
主动推送相关原理介绍:
该插件的 GitHub 地址:https://github.com/huiwang/hexo-baidu-url-submit
关于自动推送百度官网给出的解释是:自动推送是百度搜索资源平台为提高站点新增网页发现速度推出的工具,安装自动推送JS代码的网页,在页面被访问时,页面URL将立即被推送给百度
此时要注意,有些 hexo 主题集成了这项功能,比如 next 主题,在 themes\next\layout_scripts\
下有个 baidu_push.swig
文件,我们只需要把如下代码粘贴到该文件,然后在主题配置文件设置 baidu_push: true
即可
1 | {% if theme.baidu_push %} |
然而大部分主题是没有集成这项功能的,对于大部分主题来说,我们可以把以下代码粘贴到 head.ejs
文件的 <head>
与 </head>
标签之间即可,从而实现自动推送(比如我使用的是 Material X 主题,那么只需要把代码粘贴到 \themes\material-x\layout\_partial\head.ejs
中即可)
1 | <script> |
首先我们要使用以下命令生成一个网站地图:
1 | npm install hexo-generator-sitemap --save |
这里也注意一下,将根目录的 _config.yml
文件,大约第 17 行处,url 改为在百度站长平台添加的域名,也就是你网站的首页地址:
1 | # URL |
然后使用命令 hexo g -d
将网站部署上去,然后访问 你的首页/sitemap.xml
或者 你的首页/baidusitemap.xml
就可以看到网站地图了
比如我的是:https://www.itrhx.com/baidusitemap.xml 或者 https://www.itrhx.com/sitemap.xml
其中 sitemap.xml
文件是搜索引擎通用的 sitemap 文件,baidusitemap.xml
是百度专用的 sitemap 文件
然后来到百度站长平台的 sitemap 提交页面,将你的 sitemap 地址提交即可,如果成功的话状态会显示为正常,初次提交要等几分钟,sitemap.xml 相比 baidusitemap.xml 来说等待时间也会更长,如果以后你博客有新的文章或其他页面,可以点击手动更新文件,更新一下新的 sitemap
手动提交不需要其他额外操作,直接把需要收录的页面的 url 提交即可,这种方法效率较低,更新较慢,不推荐使用
提交谷歌搜索引擎比较简单,在提交之前,我们依然可以使用 site:域名
查看网站是否被收录,我的网站搭建了有差不多一年了,之前也没提交过收录,不过谷歌爬虫的确是强大,即使没有提交过,现在也能看到有一百多条结果了:
接下来我们将网站提交谷歌搜索引擎搜索,进入谷歌站长平台,登录你的谷歌账号之后会让你验证网站所有权:
有两种验证方式,分别是网域和网址前缀,两种资源类型区别如下:
说明 | 仅包含具有指定前缀(包括协议 http/https)的网址。如果希望资源匹配任何协议或子网域(http/https/www./m. 等),建议改为添加网域资源。 | 包括所有子网域(m、www 等)和多种协议(http、https、ftp)的网域级资源。 |
验证 | 多种类型 | 仅 DNS 记录验证 |
示例 | 资源 http://example.com/ ✔ http://example.com/dresses/1234 X https://example.com/dresses/1234 X http://www.example.com/dresses/1234 | 资源 example.com ✔ http://example.com/dresses/1234 ✔ https://example.com/dresses/1234 ✔ http://www.example.com/dresses/1234 ✔ http://support.m.example.com/dresses/1234 |
由对比可知选择网域资源验证方式比较好,只需要一个域名就可以匹配到多种格式的 URL,之后会给你一个 TXT 的记录值,复制它到你域名 DNS 增加一个 TXT 记录,点击验证即可
提交谷歌收录比较简单,选择站点地图,将我们之前生成的 sitemap 提交就行了,过几分钟刷新一下看到成功字样表示提交成功!
部署到 Coding Pages 的好处:国内访问速度更快,可以提交百度收录(GitHub 禁止了百度的爬取)
部署到 Coding Pages 的坏处:就今年来说,Coding 不太稳定,随时有宕机的可能,群里的朋友已经经历过几次了,不过相信以后会越来越稳定的
部署过程中常见的问题:无法实现全站 HTTPS,Coding 申请 SSL 证书失败,浏览器可能会提示不是安全链接
本文前提:你已经将 Hexo 成功部署到了 GitHub Pages,如果还没有,请参考:《使用Github Pages和Hexo搭建自己的独立博客【超级详细的小白教程】》
本文将全面讲述如何成功双线部署到 Coding Pages 和 GitHub Pages 并实现全站 HPPTS,同时解决一些常见的问题!
进入 Coding 官网,点击个人版登陆,没有账号就注册一个并登录,由于 Coding 已经被腾讯收购了,所以登录就会来到腾讯云开发者平台,点击创建项目
项目名称建议和你的用户名一致,这样做的好处是:到时候可以直接通过 user_name.coding.me
访问你的博客,如果项目名与用户名不一致,则需要通过 user_name.coding.me/project_name
才能访问,项目描述可以随便写
配置 SSH 公钥方法与 GitHub Pages 的方式差不多,点击你的头像,依次选择【个人设置】-【SSH公钥】-【新增公钥】
前面部署到 GitHub Pages 的时候就已经有了一对公钥,我们直接将该公钥粘贴进去就行,公钥名称可以随便写,选中永久有效选项
PS:公钥储存位置一般在 C:\Users\用户名\.ssh 目录下的 id_rsa.pub 文件里,用记事本打开复制其内容即可
添加公钥后,我们可以右键 Get Bash
,输入以下命令来检查是否配置成功:
1 | ssh -T git@git.coding.net |
若出现以下提示,则证明配置成功:
1 | Coding 提示: Hello XXX, You've connected to Coding.net via SSH. This is a personal key. |
进入你的项目,在右下角有选择连接方式,选择 SSH 方式(HTTPS 方式也可以,但是这种方式有时候可能连接不上,SSH 连接不容易出问题),一键复制,然后打开你本地博客根目录的 _config.yml
文件,找到 deploy
关键字,添加 coding 地址:coding: git@git.dev.tencent.com:user_name/user_name.git
,也就是刚刚复制的 SSH 地址
添加完成后先执行命令 hexo clean
清理一下缓存,然后执行命令 hexo g -d
将博客双线部署到 Coding Pages 和 GitHub Pages,如下图所示表示部署成功:
进入你的项目,在代码栏下选择 Pages 服务,一键开启 Coding Pages,等待几秒后刷新网页即可看到已经开启的 Coding Pages,到目前为止,你就可以通过 xxxx.coding.me(比如我的是 trhx.coding.me)访问你的 Coding Pages 页面了
首先在你的域名 DNS 设置中添加一条 CNAME
记录指向 xxxx.coding.me
,解析路线选择 默认
,将 GitHub 的解析路线改为 境外
,这样境外访问就会走 GitHub,境内就会走 Coding,也有人说阿里云是智能解析,自动分配路线,如果解析路线都是默认,境外访问同样会智能选择走 GitHub,境内走 Coding,我没有验证过,有兴趣的可以自己试试,我的解析如下图所示:
然后点击静态 Pages 应用右上角的设置,进入设置页面,这里要注意,如果你之前已经部署到了 GitHub Pages 并开启了 HTTPS,那么直接在设置页面绑定你自己的域名,SSL/TLS 安全证书就会显示申请错误,如下图所示,没有申请到 SSL 证书,当你访问你的网站时,浏览器就会提示不是安全连接
申请错误原因是:在验证域名所有权时会定位到 Github Pages 的主机上导致 SSL 证书申请失败
正确的做法是:先去域名 DNS 把 GitHub 的解析暂停掉,然后再重新申请 SSL 证书,大约十秒左右就能申请成功,然后开启强制 HTTPS 访问
这里也建议同时绑定有 www 前缀和没有 www 前缀的,如果要绑定没有 www 前缀的,首先要去域名 DNS 添加一个 A
记录,主机记录为 @
,记录值为你博客 IP 地址,IP 地址可以在 cmd 命令行 ping 一下得到,然后在 Coding Pages 中设置其中一个为【首选】,另一个设置【跳转至首选】,这样不管用户是否输入 www 前缀都会跳到有 www 前缀的了
在博客资源引用的时候也要注意所有资源的 URL 必须是以 https:// 开头,不然浏览器依旧会提示不安全!
至此,我们的 Hexo 博客就成功双线部署到 Coding Pages 和 GitHub Pages 了,并且也实现了全站 HPPTS,最后来一张 GitHub Pages 和 Coding Pages 在国内的速度对比图,可以明显看到速度的提升
Python3 爬虫学习笔记第十六章 —— 【数据储存系列 — Redis】
Redis 是一个基于内存的高效的键值型(key-value)非关系型数据库,它支持存储的 value 类型非常多,包括 string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合) 和 hash(哈希类型),它的性能十分优越,可以支持每秒十几万此的读/写操作,其性能远超数据库,并且还支持集群、分布式、主从同步等配置,原则上可以无限扩展,让更多的数据存储在内存中,此外,它还支持一定的事务能力,这保证了高并发的场景下数据的安全和一致性。
首先安装 Redis 和 redis-py 库,管理 Redis 可以使用可视化工具 Redis Desktop Manager,该工具现在收费了,分享个 0.8.8.384 的免费版本
安装 redis-py 库:pip install redis
Redis 官网:https://redis.io
官方文档:https://redis.io/documentation
中文官网:http://www.redis.cn
中文教程:http://www.runoob.com/redis/redis-tutorial.html
GitHub:https://github.com/antirez/redis
Redis Windows下载地址一:https://github.com/microsoftarchive/redis/releases (最新版 3.2.100,似乎不再更新)
Redis Windows下载地址二:https://github.com/tporadowski/redis/releases (最新版)
Redis Desktop Manager 官网:https://redisdesktop.com/
Redis Desktop Manager 0.8.8.384 免费版:https://pan.baidu.com/s/18MKeCqT0MG0hc89jfkpIkA (提取码:3ovc)
利用 Python 连接 Redis 示例:
1 | from redis import StrictRedis |
传入 Redis 的地址、运行端口、使用的数据库和密码, 4 个参数默认值分别为 localhost、6379、0 和 None,声明一个 StrictRedis 对象,调用 set() 方法,设置一个键值对,输出结果如下:
1 | b'TRHX' |
另外也可以使用 ConnectionPool 来连接:
1 | from redis import StrictRedis, ConnectionPool |
ConnectionPool 也支持通过 URL 来构建:
1 | redis://[:password]@host:port/db # 创建 Redis TCP 连接 |
代码示例:
1 | from redis import StrictRedis, ConnectionPool |
以下是有关的键操作、字符串操作、列表操作、集合操作、散列操作的各种方法,记录一下,方便查阅
来源:《Python3 网络爬虫开发实战(崔庆才著)》
Redis 命令参考:http://redisdoc.com/ 、http://doc.redisfans.com/
方法 | 作用 | 参数说明 | 示例 | 示例说明 | 示例结果 |
---|---|---|---|---|---|
exists(name) | 判断一个键是否存在 | name:键名 | redis.exists(‘name’) | 是否存在 name 这个键 | True |
delete(name) | 删除一个键 | name:键名 | redis.delete(‘name’) | 删除 name 这个键 | 1 |
type(name) | 判断键类型 | name:键名 | redis.type(‘name’) | 判断 name 这个键类型 | b’string’ |
keys(pattern) | 获取所有符合规则的键 | pattern:匹配规则 | redis.keys(‘n*’) | 获取所有以 n 开头的键 | [b’name’] |
randomkey() | 获取随机的一个键 | randomkey() | 获取随机的一个键 | b’name’ | |
rename(src, dst) | 重命名键 | src:原键名;dst:新键名 | redis.rename(‘name’, ‘nickname’) | 将 name 重命名为 nickname | True |
dbsize() | 获取当前数据库中键的数目 | dbsize() | 获取当前数据库中键的数目 | 100 | |
expire(name, time) | 设定键的过期时间,单位为秒 | name:键名;time:秒数 | redis.expire(‘name’, 2) | 将 name 键的过期时间设置为 2 秒 | True |
ttl(name) | 获取键的过期时间,单位为秒,-1 表示永久不过期 | name:键名 | redis.ttl(‘name’) | 获取 name 这个键的过期时间 | -1 |
move(name, db) | 将键移动到其他数据库 | name:键名;db:数据库代号 | move(‘name’, 2) | 将 name 移动到 2 号数据库 | True |
flushdb() | 删除当前选择数据库中的所有键 | flushdb() | 删除当前选择数据库中的所有键 | True | |
flushall() | 删除所有数据库中的所有键 | flushall() | 删除所有数据库中的所有键 | True |
方法 | 作用 | 参数说明 | 示例 | 示例说明 | 示例结果 |
---|---|---|---|---|---|
set(name, value) | 给数据库中键名为 name 的 string 赋予值 value | name:键名;value:值 | redis.set(‘name’, ‘Bob’) | 给 name 这个键的 value 赋值为 Bob | True |
get(name) | 返回数据库中键名为 name 的 string 的 value | name:键名 | redis.get(‘name’) | 返回 name 这个键的 value | b’Bob’ |
getset(name, value) | 给数据库中键名为 name 的 string 赋予值 value 并返回上次的 value | name:键名;value:新值 | redis.getset(‘name’, ‘Mike’) | 赋值 name 为 Mike 并得到上次的 value | b’Bob’ |
mget(keys, *args) | 返回多个键对应的 value 组成的列表 | keys:键名序列 | redis.mget([‘name’, ‘nickname’]) | 返回 name 和 nickname 的 value | [b’Mike’, b’Miker’] |
setnx(name, value) | 如果不存在这个键值对,则更新 value,否则不变 | name:键名 | redis.setnx(‘newname’, ‘James’) | 如果 newname 这个键不存在,则设置值为 James | 第一次运行结果是 True,第二次运行结果是 False |
setex(name, time, value) | 设置可以对应的值为 string 类型的 value,并指定此键值对应的有效期 | name:键名;time:有效期;value:值 | redis.setex(‘name’, 1, ‘James’) | 将 name 这个键的值设为 James,有效期为 1 秒 | True |
setrange(name, offset, value) | 设置指定键的 value 值的子字符串 | name:键名;offset:偏移量;value:值 | redis.set(‘name’, ‘Hello’) redis.setrange (‘name’, 6, ‘World’) | 设置 name 为 Hello 字符串,并在 index 为 6 的位置补 World | 11,修改后的字符串长度 |
mset(mapping) | 批量赋值 | mapping:字典或关键字参数 | redis.mset({‘name1’: ‘Durant’, ‘name2’: ‘James’}) | 将 name1 设为 Durant,name2 设为 James | True |
msetnx(mapping) | 键均不存在时才批量赋值 | mapping:字典或关键字参数 | redis.msetnx({‘name3’: ‘Smith’, ‘name4’: ‘Curry’}) | 在 name3 和 name4 均不存在的情况下才设置二者值 | True |
incr(name, amount=1) | 键名为 name 的 value 增值操作,默认为 1,键不存在则被创建并设为 amount | name:键名;amount:增长的值 | redis.incr(‘age’, 1) | age 对应的值增 1,若不存在,则会创建并设置为 1 | 1,即修改后的值 |
decr(name, amount=1) | 键名为 name 的 value 减值操作,默认为 1,键不存在则被创建并将 value 设置为 - amount | name:键名;amount:减少的值 | redis.decr(‘age’, 1) | age 对应的值减 1,若不存在,则会创建并设置为-1 | -1,即修改后的值 |
append(key, value) | 键名为 key 的 string 的值附加 value | key:键名 | redis.append(‘nickname’, ‘OK’) | 向键名为 nickname 的值后追加 OK | 13,即修改后的字符串长度 |
substr(name, start, end=-1) | 返回键名为 name 的 string 的子字符串 | name:键名;start:起始索引;end:终止索引,默认为-1,表示截取到末尾 | redis.substr(‘name’, 1, 4) | 返回键名为 name 的值的字符串,截取索引为 1~4 的字符 | b’ello’ |
getrange(key, start, end) | 获取键的 value 值从 start 到 end 的子字符串 | key:键名;start:起始索引;end:终止索引 | redis.getrange(‘name’, 1, 4) | 返回键名为 name 的值的字符串,截取索引为 1~4 的字符 | b’ello |
方法 | 作用 | 参数说明 | 示例 | 示例说明 | 示例结果 |
---|---|---|---|---|---|
hset(name, key, value) | 向键名为 name 的散列表中添加映射 | name:键名;key:映射键名;value:映射键值 | hset(‘price’, ‘cake’, 5) | 向键名为 price 的散列表中添加映射关系,cake 的值为 5 | 1,即添加的映射个数 |
hsetnx(name, key, value) | 如果映射键名不存在,则向键名为 name 的散列表中添加映射 | name:键名;key:映射键名;value:映射键值 | hsetnx(‘price’, ‘book’, 6) | 向键名为 price 的散列表中添加映射关系,book 的值为 6 | 1,即添加的映射个数 |
hget(name, key) | 返回键名为 name 的散列表中 key 对应的值 | name:键名;key:映射键名 | redis.hget(‘price’, ‘cake’) | 获取键名为 price 的散列表中键名为 cake 的值 | 5 |
hmget(name, keys, *args) | 返回键名为 name 的散列表中各个键对应的值 | name:键名;keys:键名序列 | redis.hmget(‘price’, [‘apple’, ‘orange’]) | 获取键名为 price 的散列表中 apple 和 orange 的值 | [b’3’, b’7’] |
hmset(name, mapping) | 向键名为 name 的散列表中批量添加映射 | name:键名;mapping:映射字典 | redis.hmset(‘price’, {‘banana’: 2, ‘pear’: 6}) | 向键名为 price 的散列表中批量添加映射 | True |
hincrby(name, key, amount=1) | 将键名为 name 的散列表中映射的值增加 amount | name:键名;key:映射键名;amount:增长量 | redis.hincrby(‘price’, ‘apple’, 3) | key 为 price 的散列表中 apple 的值增加 3 | 6,修改后的值 |
hexists(name, key) | 键名为 name 的散列表中是否存在键名为键的映射 | name:键名;key:映射键名 | redis.hexists(‘price’, ‘banana’) | 键名为 price 的散列表中 banana 的值是否存在 | True |
hdel(name, *keys) | 在键名为 name 的散列表中,删除键名为键的映射 | name:键名;keys:键名序列 | redis.hdel(‘price’, ‘banana’) | 从键名为 price 的散列表中删除键名为 banana 的映射 | True |
hlen(name) | 从键名为 name 的散列表中获取映射个数 | name:键名 | redis.hlen(‘price’) | 从键名为 price 的散列表中获取映射个数 | 6 |
hkeys(name) | 从键名为 name 的散列表中获取所有映射键名 | name:键名 | redis.hkeys(‘price’) | 从键名为 price 的散列表中获取所有映射键名 | [b’cake’, b’book’, b’banana’, b’pear’] |
hvals(name) | 从键名为 name 的散列表中获取所有映射键值 | name:键名 | redis.hvals(‘price’) | 从键名为 price 的散列表中获取所有映射键值 | [b’5’, b’6’, b’2’, b’6’] |
hgetall(name) | 从键名为 name 的散列表中获取所有映射键值对 | name:键名 | redis.hgetall(‘price’) | 从键名为 price 的散列表中获取所有映射键值对 | {b’cake’: b’5’, b’book’: b’6’, b’orange’: b’7’, b’pear’: b’6’} |
方法 | 作用 | 参数说明 | 示例 | 示例说明 | 示例结果 |
---|---|---|---|---|---|
rpush(name, *values) | 在键名为 name 的列表末尾添加值为 value 的元素,可以传多个 | name:键名;values:值 | redis.rpush(‘list’, 1, 2, 3) | 向键名为 list 的列表尾添加 1、2、3 | 3,列表大小 |
lpush(name, *values) | 在键名为 name 的列表头添加值为 value 的元素,可以传多个 | name:键名;values:值 | redis.lpush(‘list’, 0) | 向键名为 list 的列表头部添加 0 | 4,列表大小 |
llen(name) | 返回键名为 name 的列表的长度 | name:键名 | redis.llen(‘list’) | 返回键名为 list 的列表的长度 | 4 |
lrange(name, start, end) | 返回键名为 name 的列表中 start 至 end 之间的元素 | name:键名;start:起始索引;end:终止索引 | redis.lrange(‘list’, 1, 3) | 返回起始索引为 1 终止索引为 3 的索引范围对应的列表 | [b’3’, b’2’, b’1’] |
ltrim(name, start, end) | 截取键名为 name 的列表,保留索引为 start 到 end 的内容 | name:键名;start:起始索引;end:终止索引 | ltrim(‘list’, 1, 3) | 保留键名为 list 的索引为 1 到 3 的元素 | True |
lindex(name, index) | 返回键名为 name 的列表中 index 位置的元素 | name:键名;index:索引 | redis.lindex(‘list’, 1) | 返回键名为 list 的列表索引为 1 的元素 | b’2’ |
lset(name, index, value) | 给键名为 name 的列表中 index 位置的元素赋值,越界则报错 | name:键名;index:索引位置;value:值 | redis.lset(‘list’, 1, 5) | 将键名为 list 的列表中索引为 1 的位置赋值为 5 | True |
lrem(name, count, value) | 删除 count 个键的列表中值为 value 的元素 | name:键名;count:删除个数;value:值 | redis.lrem(‘list’, 2, 3) | 将键名为 list 的列表删除两个 3 | 1,即删除的个数 |
lpop(name) | 返回并删除键名为 name 的列表中的首元素 | name:键名 | redis.lpop(‘list’) | 返回并删除名为 list 的列表中的第一个元素 | b’5’ |
rpop(name) | 返回并删除键名为 name 的列表中的尾元素 | name:键名 | redis.rpop(‘list’) | 返回并删除名为 list 的列表中的最后一个元素 | b’2’ |
blpop(keys, timeout=0) | 返回并删除名称在 keys 中的 list 中的首个元素,如果列表为空,则会一直阻塞等待 | keys:键名序列;timeout:超时等待时间,0 为一直等待 | redis.blpop(‘list’) | 返回并删除键名为 list 的列表中的第一个元素 | [b’5’] |
brpop(keys, timeout=0) | 返回并删除键名为 name 的列表中的尾元素,如果 list 为空,则会一直阻塞等待 | keys:键名序列;timeout:超时等待时间,0 为一直等待 | redis.brpop(‘list’) | 返回并删除名为 list 的列表中的最后一个元素 | [b’2’] |
rpoplpush(src, dst) | 返回并删除名称为 src 的列表的尾元素,并将该元素添加到名称为 dst 的列表头部 | src:源列表的键;dst:目标列表的 key | redis.rpoplpush(‘list’, ‘list2’) | 将键名为 list 的列表尾元素删除并将其添加到键名为 list2 的列表头部,然后返回 | b’2’ |
方法 | 作用 | 参数说明 | 示例 | 示例说明 | 示例结果 |
---|---|---|---|---|---|
sadd(name, *values) | 向键名为 name 的集合中添加元素 | name:键名;values:值,可为多个 | redis.sadd(‘tags’, ‘Book’, ‘Tea’, ‘Coffee’) | 向键名为 tags 的集合中添加 Book、Tea 和 Coffee 这 3 个内容 | 3,即插入的数据个数 |
srem(name, *values) | 从键名为 name 的集合中删除元素 | name:键名;values:值,可为多个 | redis.srem(‘tags’, ‘Book’) | 从键名为 tags 的集合中删除 Book | 1,即删除的数据个数 |
spop(name) | 随机返回并删除键名为 name 的集合中的一个元素 | name:键名 | redis.spop(‘tags’) | 从键名为 tags 的集合中随机删除并返回该元素 | b’Tea’ |
smove(src, dst, value) | 从 src 对应的集合中移除元素并将其添加到 dst 对应的集合中 | src:源集合;dst:目标集合;value:元素值 | redis.smove(‘tags’, ‘tags2’, ‘Coffee’) | 从键名为 tags 的集合中删除元素 Coffee 并将其添加到键为 tags2 的集合 | True |
scard(name) | 返回键名为 name 的集合的元素个数 | name:键名 | redis.scard(‘tags’) | 获取键名为 tags 的集合中的元素个数 | 3 |
sismember(name, value) | 测试 member 是否是键名为 name 的集合的元素 | name:键值 | redis.sismember(‘tags’, ‘Book’) | 判断 Book 是否是键名为 tags 的集合元素 | True |
sinter(keys, *args) | 返回所有给定键的集合的交集 | keys:键名序列 | redis.sinter([‘tags’, ‘tags2’]) | 返回键名为 tags 的集合和键名为 tags2 的集合的交集 | {b’Coffee’} |
sinterstore(dest, keys, *args) | 求交集并将交集保存到 dest 的集合 | dest:结果集合;keys:键名序列 | redis.sinterstore (‘inttag’, [‘tags’, ‘tags2’]) | 求键名为 tags 的集合和键名为 tags2 的集合的交集并将其保存为 inttag | 1 |
sunion(keys, *args) | 返回所有给定键的集合的并集 | keys:键名序列 | redis.sunion([‘tags’, ‘tags2’]) | 返回键名为 tags 的集合和键名为 tags2 的集合的并集 | {b’Coffee’, b’Book’, b’Pen’} |
sunionstore(dest, keys, *args) | 求并集并将并集保存到 dest 的集合 | dest:结果集合;keys:键名序列 | redis.sunionstore (‘inttag’, [‘tags’, ‘tags2’]) | 求键名为 tags 的集合和键名为 tags2 的集合的并集并将其保存为 inttag | 3 |
sdiff(keys, *args) | 返回所有给定键的集合的差集 | keys:键名序列 | redis.sdiff([‘tags’, ‘tags2’]) | 返回键名为 tags 的集合和键名为 tags2 的集合的差集 | {b’Book’, b’Pen’} |
sdiffstore(dest, keys, *args) | 求差集并将差集保存到 dest 集合 | dest:结果集合;keys:键名序列 | redis.sdiffstore (‘inttag’, [‘tags’, ‘tags2’]) | 求键名为 tags 的集合和键名为 tags2 的集合的差集并将其保存为 inttag | 3 |
smembers(name) | 返回键名为 name 的集合的所有元素 | name:键名 | redis.smembers(‘tags’) | 返回键名为 tags 的集合的所有元素 | {b’Pen’, b’Book’, b’Coffee’} |
srandmember(name) | 随机返回键名为 name 的集合中的一个元素,但不删除元素 | name:键值 | redis.srandmember(‘tags’) | 随机返回键名为 tags 的集合中的一个元素 | Srandmember (name) |
方法 | 作用 | 参数说明 | 示例 | 示例说明 | 示例结果 |
---|---|---|---|---|---|
zadd(name, args, *kwargs) | 向键名为 name 的 zset 中添加元素 member,score 用于排序。如果该元素存在,则更新其顺序 | name:键名;args:可变参数 | redis.zadd(‘grade’, 100, ‘Bob’, 98, ‘Mike’) | 向键名为 grade 的 zset 中添加 Bob(其 score 为 100),并添加 Mike(其 score 为 98) | 2,即添加的元素个数 |
zrem(name, *values) | 删除键名为 name 的 zset 中的元素 | name:键名;values:元素 | redis.zrem(‘grade’, ‘Mike’) | 从键名为 grade 的 zset 中删除 Mike | 1,即删除的元素个数 |
zincrby(name, value, amount=1) | 如果在键名为 name 的 zset 中已经存在元素 value,则将该元素的 score 增加 amount;否则向该集合中添加该元素,其 score 的值为 amount | name:键名;value:元素;amount:增长的 score 值 | redis.zincrby(‘grade’, ‘Bob’, -2) | 键名为 grade 的 zset 中 Bob 的 score 减 2 | 98.0,即修改后的值 |
zrank(name, value) | 返回键名为 name 的 zset 中元素的排名,按 score 从小到大排序,即名次 | name:键名;value:元素值 | redis.zrank(‘grade’, ‘Amy’) | 得到键名为 grade 的 zset 中 Amy 的排名 | 1 |
zrevrank(name, value) | 返回键为 name 的 zset 中元素的倒数排名(按 score 从大到小排序),即名次 | name:键名;value:元素值 | redis.zrevrank (‘grade’, ‘Amy’) | 得到键名为 grade 的 zset 中 Amy 的倒数排名 | 2 |
zrevrange(name, start, end, withscores= False) | 返回键名为 name 的 zset(按 score 从大到小排序)中 index 从 start 到 end 的所有元素 | name:键值;start:开始索引;end:结束索引;withscores:是否带 score | redis.zrevrange (‘grade’, 0, 3) | 返回键名为 grade 的 zset 中前四名元素 | [b’Bob’, b’Mike’, b’Amy’, b’James’] |
zrangebyscore (name, min, max, start=None, num=None, withscores=False) | 返回键名为 name 的 zset 中 score 在给定区间的元素 | name:键名;min:最低 score;max:最高 score;start:起始索引;num:个数;withscores:是否带 score | redis.zrangebyscore (‘grade’, 80, 95) | 返回键名为 grade 的 zset 中 score 在 80 和 95 之间的元素 | [b’Bob’, b’Mike’, b’Amy’, b’James’] |
zcount(name, min, max) | 返回键名为 name 的 zset 中 score 在给定区间的数量 | name:键名;min:最低 score;max:最高 score | redis.zcount(‘grade’, 80, 95) | 返回键名为 grade 的 zset 中 score 在 80 到 95 的元素个数 | 2 |
zcard(name) | 返回键名为 name 的 zset 的元素个数 | name:键名 | redis.zcard(‘grade’) | 获取键名为 grade 的 zset 中元素的个数 | 3 |
zremrangebyrank (name, min, max) | 删除键名为 name 的 zset 中排名在给定区间的元素 | name:键名;min:最低位次;max:最高位次 | redis.zremrangebyrank (‘grade’, 0, 0) | 删除键名为 grade 的 zset 中排名第一的元素 | 1,即删除的元素个数 |
zremrangebyscore (name, min, max) | 删除键名为 name 的 zset 中 score 在给定区间的元素 | name:键名;min:最低 score;max:最高 score | redis.zremrangebyscore (‘grade’, 80, 90) | 删除 score 在 80 到 90 之间的元素 | 1,即删除的元素个数 |
RedisDump 是 Redis 一个数据导入导出工具,是基于 Ruby 实现的,首先访问 Ruby 官网安装对应操作系统的 Ruby:http://www.ruby-lang.org/zh_cn/downloads/ ,安装完成即可使用 gem 命令,该命令类似于 Python 当中的 pip 命令,使用 gem install redis-dump
即可完成 RedisDump 的安装,安装完成后就可以使用导出数据 redis-dump
命令和导入数据 redis-load
命令了
在命令行输入 redis-dump -h
可以查看:
1 | Usage: E:/Ruby26-x64/bin/redis-dump [global options] COMMAND [command options] |
命令解释:
导出数据示例:
1 | redis-dump |
输出示例:
1 | 导出所有数据 |
导出所有数据为 JSON 文件:
1 | redis-dump -u 127.0.0.1:6379 > db_full.json |
该命令将会在当前目录生成一个名为 db_full.json 的文件,文件内容如下:
1 | {"db":0,"key":"name5","ttl":-1,"type":"string","value":"DDD","size":3} |
使用参数 -d 指定某个数据库的所有数据导出为 JSON 文件:
1 | redis-dump -u 127.0.0.1:6379 -d 4 > db_db4.json |
该命令会将 4 号数据库的数据导出到 db_db4.json 文件:
1 | {"db":4,"key":"age","ttl":-1,"type":"string","value":"20","size":2} |
使用参数 -f 过滤数据,只导出特定的数据:
1 | redis-dump -u 127.0.0.1:6379 -f name > db_name.json |
该命令会导出 key 包含 name 的数据到 db_name.json 文件:
1 | {"db":0,"key":"name5","ttl":-1,"type":"string","value":"DDD","size":3} |
在命令行输入 redis-load -h
可以查看:
1 | redis-load --help |
命令解释:
导入示例:
1 | 将 test.json 文件所有内容导入到数据库 |
另外,以下方法也能导入数据:
1 | 将 test.json 文件所有内容导入到数据库 |
注意:cat
是 Linux 系统专有的命令,在 Windows 系统里没有 cat
这个命令,可以使用 Windows 批处理命令 type
代替 cat
Python3 爬虫学习笔记第十五章 —— 【代理的基本使用】
大多数网站都有反爬虫机制,如果一段时间内同一个 IP 发送的请求过多,服务器就会拒绝访问,直接禁封该 IP,此时,设置代理即可解决这个问题,网络上有许多免费代理和付费代理,比如西刺代理,全网代理 IP,快代理等,设置代理需要用到的就是代理 IP 地址和端口号,如果电脑上装有代理软件(例如:酸酸乳SSR),软件一般会在本机创建 HTTP 或 SOCKS 代理服务,直接使用此代理也可以
1 | from urllib.error import URLError |
http://httpbin.org/get 是一个请求测试站点,借助 ProxyHandler 设置代理,参数为字典类型,键名为协议类型,键值为代理,代理的写法:proxy = '127.0.0.1:1080'
,其中 127.0.0.1 为 IP 地址,1080 为端口号,这里表示本机的代理软件已经在本地 1080 端口创建了代理服务,代理前面需要加上 http 或者 https 协议,当请求的链接为 http 协议时,ProxyHandler 会自动调用 http 代理,同理,当请求的链接为 https 协议时,ProxyHandler 会自动调用 https 代理,build_opener()
方法传入 ProxyHandler 对象来创建一个 opener,调用 open()
方法传入一个 url 即可通过代理访问该链接,运行结果为一个 JSON,origin 字段为此时客户端的 IP
1 | { |
如果是需要认证的代理,只需要在代理前面加入代理认证的用户名密码即可:
1 | from urllib.error import URLError |
如果代理是 SOCKS5 类型,需要用到 socks
模块,设置代理方法如下:
扩展:SOCKS5 是一个代理协议,它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色,使得内部网中的前端机器变得能够访问 Internet 网中的服务器,或者使通讯更加安全
1 | import socks |
requests 库使用代理只需要传入 proxies 参数即可:
1 | import requests |
输出结果:
1 | { |
同样的,如果是需要认证的代理,也只需要在代理前面加入代理认证的用户名密码即可:
1 | import requests |
如果代理是 SOCKS5 类型,需要用到 requests[socks]
模块或者 socks
模块,使用 requests[socks]
模块时设置代理方法如下:
1 | import requests |
使用 socks
模块时设置代理方法如下(此类方法为全局设置):
1 | import requests |
1 | from selenium import webdriver |
通过 ChromeOptions 来设置代理,在创建 Chrome 对象的时候用 chrome_options 参数传递即可,访问目标链接后显示如下信息:
1 | { |
如果是认证代理,则设置方法如下:
1 | from selenium import webdriver |
需要在本地创建一个 manifest.json 配置文件和 background.js 脚本来设置认证代理。运行代码之后本地会生成一个 proxy_auth_plugin.zip 文件来保存当前配置
借助 service_args 参数,也就是命令行参数即可设置代理:
1 | from selenium import webdriver |
运行结果:
1 | <html><head></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">{ |
如果是需要认证的代理,只需要在 service_args 参数加入 –proxy-auth 选项即可:
1 | from selenium import webdriver |
Python3 爬虫学习笔记第十四章 —— 【验证码对抗系列 — 点触验证码】
点触验证码是由杭州微触科技有限公司研发的新一代的互联网验证码,使用点击的形式完成验证,采用专利的印刷算法以及加密算法,保证每次请求到的验证图具有极高的安全性,常见的点触验证码如下:
点触验证码相对其他类型验证码比较复杂,如果依靠 OCR 图像识别点触验证码,则识别难度非常大,此时就要用到互联网的验证码服务平台,这些服务平台全部都是人工在线识别,准确率非常高,原理就是先将验证码图片提交给平台,平台会返回识别结果在图片中的坐标位置,然后我们再解析坐标模拟点击即可,常见的打码平台有超级鹰、云打码等,打码平台是收费的,拿超级鹰来说,1元 = 1000题分,识别一次验证码将花费一定的题分,不同类型验证码需要的题分不同,验证码越复杂所需题分越高,比如 7 位中文汉字需要 70 题分,常见 4 ~ 6 位英文数字只要 10 题分,其他打码平台价格也都差不多
以下以超级鹰打码平台和中国铁路12306官网来做练习
首先在超级鹰打码平台注册账号并申请一个软件 ID,官网:http://www.chaojiying.com/ ,先充值一块钱得到 1000 题分,观察 12306 官网,发现验证码是要我们点击所有满足条件的图片,一般有 1~4 张图片满足要求,由此可确定在超级鹰打码平台的验证码类型为 9004(坐标多选,返回1~4个坐标,如:x1,y1|x2,y2|x3,y3), 获取其 Python API:http://www.chaojiying.com/download/Chaojiying_Python.rar ,然后用 Selenium 模拟登陆,获取到验证码,并将验证码发送给超级鹰后台,返回识别图片的坐标,最后模拟点击即可,整个过程的实现由主程序 12306.py
和超级鹰 API chaojiying.py
组成
整个程序包含的函数:
1 | def __init__(): 初始化 WebDriver、Chaojiying 对象等 |
整个程序用到的库:
1 | import time |
1 | if __name__ == '__main__': |
1 | USERNAME = '155********' |
定义 12306 账号(USERNAME
)、密码(PASSWORD
)、超级鹰用户名(CHAOJIYING_USERNAME
)、超级鹰登录密码(CHAOJIYING_PASSWORD
)、超级鹰软件 ID(CHAOJIYING_SOFT_ID
)、验证码类型(CHAOJIYING_KIND
),登录链接 url:https://kyfw.12306.cn/otn/resources/login.html ,谷歌浏览器驱动的目录(path
),浏览器启动参数,并将相关参数传递给超级鹰 API
1 | def crack(self): |
调用 open()
函数输入账号密码
调用 get_touclick_image()
函数获取验证码图片
利用超级鹰 Python API PostPic()
方法即可把图片发送给超级鹰后台,发送的图像是字节流格式,返回的结果是一个 JSON,如果识别成功,典型的返回结果类似于:{'err_no': 0, 'err_str': 'OK', 'pic_id': '6002001380949200001', 'pic_str': '132,127|56,77', 'md5': '1f8e1d4bef8b11484cb1f1f34299865b'}
,其中,pic_str 就是识别的文字的坐标,是以字符串形式返回的,每个坐标都以 | 分隔
调用 get_points()
函数解析超级鹰识别结果
调用 touch_click_words()
函数对符合要求的图片进行点击,然后点击登陆按钮模拟登陆
使用 try-except
语句判断是否出现了用户信息,判断依据是是否有用户姓名的出现,出现的姓名和实际姓名一致则登录成功,如果失败了就重试,超级鹰会返回该分值
1 | def open(self): |
分析页面可知,登陆页面 URL 为:https://kyfw.12306.cn/otn/resources/login.html ,该页面默认出现的是扫描二维码登陆,所以要先点击账号登录,找到该 CSS 元素为 login-hd-account
,调用 click()
方法实现模拟点击,此时出现账号密码输入框,同样找到其 ID 分别为 J-userName
和 J-password
,调用 send_keys()
方法输入账号密码
1 | def get_screenshot(self): |
对整个页面进行截图
1 | def get_touclick_element(self): |
同样分析页面,验证码所在位置的 CSS 为 login-pwd-code
1 | def get_position(self): |
location 属性可以返回该图片对象在浏览器中的位置,坐标轴是以屏幕左上角为原点,x 轴向右递增,y 轴向下递增,size 属性可以返回该图片对象的高度和宽度,由此可以得到验证码的位置信息
1 | def get_touclick_image(self, name='12306.png'): |
根据验证码的坐标信息,对页面截图进行剪裁,得到验证码部分,将其保存为 12306.png
1 | def get_points(self, captcha_result): |
get_points()
方法将超级鹰的验证码识别结果变成列表的形式
1 | def touch_click_words(self, locations): |
touch_click_words()
方法通过调用 move_to_element_with_offset()
方法依次传入解析后的坐标,点击即可
1 | def login(self): |
分析页面,找到登陆按钮的 ID 为 J-login
,调用 click()
方法模拟点击按钮实现登录
最终实现效果图:(关键信息已经过打码处理)
12306.py
1 | import time |
chaojiying.py
1 | import requests |
Python3 爬虫学习笔记第十三章 —— 【验证码对抗系列 — 滑动验证码】
滑动验证码属于行为式验证码,需要通过用户的操作行为来完成验证,一般是根据提示用鼠标将滑块拖动到指定的位置完成验证,此类验证码背景图片采用多种图像加密技术,且添加了很多随机效果,能有效防止OCR文字识别,另外,验证码上的文字采用了随机印刷技术,能够随机采用多种字体、多种变形的实时随机印刷,防止暴力破解;斗鱼、哔哩哔哩、淘宝等平台都使用了滑动验证码
利用自动化测试工具 Selenium 直接模拟人的行为方式来完成验证,首先要分析页面,想办法找到滑动验证码的完整图片、带有缺口的图片和需要滑动的图片,通过对比原始的图片和带滑块缺口的图片的像素,像素不同的地方就是缺口位置,计算出滑块缺口的位置,得到所需要滑动的距离,最后利用 Selenium 进行对滑块的拖拽,拖拽时要模仿人的行为,由于有个对准过程,所以是先快后慢,匀速移动、随机速度移动都不会成功
以下以哔哩哔哩为例来做模拟登录练习
首先使用 Selenium 模拟登陆 bilibili,自动输入账号密码,查找到登陆按钮并点击,使其出现滑动验证码,此时分析页面,滑动验证组件是由3个 canvas 组成,分别代表完整图片、带有缺口的图片和需要滑动的图片,3个 canvas 元素包含 CSS display
属性,display:block
为可见,display:none
为不可见,分别获取三张图片时要将其他两张图片设置为 display:none
,获取元素位置后即可对图片截图并保存,通过图片像素对比,找到缺口位置即为滑块要移动的距离,随后构造滑动轨迹,按照先加速后减速的方式移动滑块完成验证。
整个程序包含的函数:
1 | def init(): 初始化函数,定义全局变量 |
整个程序用到的库:
1 | from selenium import webdriver |
1 | if __name__ == '__main__': |
1 | def init(): |
global 关键字定义了全局变量,随后是登录页面url、谷歌浏览器驱动的目录path、实例化 Chrome 浏览器、设置浏览器分辨率最大化、用户名、密码、WebDriverWait() 方法设置等待超时
1 | def login(): |
等待用户名输入框和密码输入框对应的 ID 节点加载出来,分析页面可知,用户名输入框 id="login-username"
,密码输入框 id="login-passwd"
,获取这两个节点,调用 send_keys()
方法输入用户名和密码,随后获取登录按钮,分析页面可知登录按钮 class="btn btn-login"
,随机产生一个数并将其扩大三倍作为暂停时间,最后调用 click()
方法实现登录按钮的点击
1 | def find_element(): |
我们要获取验证码的三张图片,分别是完整的图片、带有缺口的图片和需要滑动的图片,分析页面代码,这三张图片是由 3 个 canvas 组成,3 个 canvas 元素包含 CSS display
属性,display:block
为可见,display:none
为不可见,在分别获取三张图片时要将其他两张图片设置为 display:none
,这样做才能单独提取到每张图片,定位三张图片的 class 分别为:带有缺口的图片(c_background):geetest_canvas_bg geetest_absolute
、需要滑动的图片(c_slice):geetest_canvas_slice geetest_absolute
、完整图片(c_full_bg):geetest_canvas_fullbg geetest_fade geetest_absolute
,随后传值给 save_screenshot()
函数,进一步对验证码进行处理
1 | # 设置元素不可见 |
1 | def save_screenshot(obj, name): |
location
属性可以返回该图片对象在浏览器中的位置,坐标轴是以屏幕左上角为原点,x轴向右递增,y轴向下递增,size
属性可以返回该图片对象的高度和宽度,由此可以得到验证码的位置信息,首先调用 save_screenshot()
属性对整个页面截图并保存,然后向 crop()
方法传入验证码的位置信息,由位置信息再对验证码进行剪裁并保存
1 | def slide(): |
向 get_distance()
函数传入完整的图片和缺口图片,计算滑块需要滑动的距离,再把距离信息传入 get_trace()
函数,构造滑块的移动轨迹,最后根据轨迹信息调用 move_to_gap()
函数移动滑块完成验证
1 | def get_distance(bg_image, fullbg_image): |
get_distance()
方法即获取缺口位置的方法,此方法的参数是两张图片,一张为完整的图片,另一张为带缺口的图片,distance 为滑块的初始位置,遍历两张图片的每个像素,利用 is_pixel_equal()
像素判断函数判断两张图片同一位置的像素是否相同,比较两张图 RGB 的绝对值是否均小于定义的阈值 threshold,如果绝对值均在阈值之内,则代表像素点相同,继续遍历,否则代表不相同的像素点,即缺口的位置
1 | def is_pixel_equal(bg_image, fullbg_image, x, y): |
将完整图片和缺口图片两个对象分别赋值给变量 bg_image和 fullbg_image,接下来对比图片获取缺口。我们在这里遍历图片的每个坐标点,获取两张图片对应像素点的 RGB 数据,判断像素的各个颜色之差,abs()
用于取绝对值,如果二者的 RGB 数据差距在一定范围内,那就代表两个像素相同,继续比对下一个像素点,如果差距超过一定范围,则代表像素点不同,当前位置即为缺口位置
1 | def get_trace(distance): |
get_trace()
方法传入的参数为移动的总距离,返回的是运动轨迹,运动轨迹用 trace 表示,它是一个列表,列表的每个元素代表每次移动多少距离,利用 Selenium 进行对滑块的拖拽时要模仿人的行为,由于有个对准过程,所以是先快后慢,匀速移动、随机速度移动都不会成功,因此要设置一个加速和减速的距离,这里设置加速距离 faster_distance
是总距离 distance
的4/5倍,滑块滑动的加速度用 a 来表示,当前速度用 v 表示,初速度用 v0 表示,位移用 move 表示,所需时间用 t 表示,它们之间满足以下关系:
1 | move = v0 * t + 0.5 * a * t * t |
设置初始位置、初始速度、时间间隔分别为0, 0, 0.1,加速阶段和减速阶段的加速度分别设置为20和-20,直到运动轨迹达到总距离时,循环终止,最后得到的 trace 记录了每个时间间隔移动了多少位移,这样滑块的运动轨迹就得到了
1 | def move_to_gap(trace): |
传入的参数为运动轨迹,首先查找到滑动按钮,然后调用 ActionChains 的 click_and_hold()
方法按住拖动底部滑块,perform()
方法用于执行,遍历运动轨迹获取每小段位移距离,调用 move_by_offset()
方法移动此位移,最后调用 release()
方法松开鼠标即可
最终实现效果图:(关键信息已经过打码处理)
1 | from selenium import webdriver |
Python3 爬虫学习笔记第十二章 —— 【验证码对抗系列 — 图形验证码】
普通图形验证码一般由四位纯数字、纯字母或者字母数字组合构成,是最常见的验证码,也是最简单的验证码,利用 tesserocr 或者 pytesseract 库即可识别此类验证码,前提是已经安装好 Tesseract-OCR 软件
简单示例:
1 | import tesserocr |
新建一个 Image 对象,调用 tesserocr 的 image_to_text()
方法,传入 Image 对象即可完成识别,另一种方法:
1 | import tesserocr |
简单示例:
1 | import pytesseract |
pytesseract 的各种方法:
有关参数:
image_to_data(image, lang='', config='', nice=0, output_type=Output.STRING)
lang 参数,常见语言代码如下:
利用 Image 对象的 convert()
方法传入不同参数可以对验证码做一些额外的处理,如转灰度、二值化等操作,经过处理过后的验证码会更加容易被识别,识别准确度更高,各种参数及含义:
示例:
1 | import pytesseract |
Image 对象的 convert()
方法参数传入 L,即可将图片转化为灰度图像,转换前后对比:
1 | import pytesseract |
Image 对象的 convert()
方法参数传入 1,即可将图片进行二值化处理,处理前后对比:
tesserocr GitHub:https://github.com/sirfz/tesserocr
tesserocr PyPI:https://pypi.python.org/pypi/tesserocr
pytesserocr GitHub:https://github.com/madmaze/pytesseract
pytesserocr PyPI:https://pypi.org/project/pytesseract/
Tesseract-OCR 下载地址:http://digi.bib.uni-mannheim.de/tesseract
tesseract GitHub:https://github.com/tesseract-ocr/tesseract
tesseract 语言包:https://github.com/tesseract-ocr/tessdata
tesseract 文档:https://github.com/tesseract-ocr/tesseract/wiki/Documentation