TRHX'S BLOG 一入 IT 深似海 从此学习无绝期 2020-02-07T05:13:39.398Z https://www.itrhx.com/ TRHX Hexo 用 VPS 搭建一个自己的 SSR 服务器 https://www.itrhx.com/2020/01/10/A61-build-a-SSR-server-with-VPS/ 2020-01-10T13:38:13.786Z 2020-02-07T05:13:39.398Z

俗话说得好:预先善其事,必先利其器,作为一个程序员,经常会用到 GitHub、Google、Stack Overflow 啥的,由于国内政策原因,想要访问国外网站就得科学上网,最常见的工具就是 ShadowsocksR,又被称为酸酸乳、SSR、小飞机,目前市面上有很多很多的机场,价格也不是很高,完全可以订阅别人的,但是订阅别人的,数据安全没有保障,有可能你的浏览历史啥的别人都能掌握,别人也有随时跑路的可能,总之,只有完全属于自己的东西才是最香的!


购买 VPS

VPS(Virtual Private Server)即虚拟专用服务器技术,在购买 VPS 服务器的时候要选择国外的,推荐 Vultr,国际知名,性价比比较高,最低有$2.5/月、$3.5/月的,个人用的话应该足够了。


01

点击链接注册 Vultr 账号:https://www.vultr.com/?ref=8367048,目前新注册用户充值10刀可以赠送50刀,注册完毕之后来到充值页面,最低充值10刀,可以选择支付宝或者微信支付。


02

充值完毕之后,点击左侧 Products,选择服务器,一共有16个地区的,选择不同地区的服务器,最后的网速也有差别,那如何选择一个速度最优的呢?很简单,你可以一次性选择多个服务器,都部署上去,搭建完毕之后,测试其速度,选择最快的,最后再把其他的都删了,可能你会想,部署多个,那费用岂不是很贵,这里注意,虽然写的是多少钱一个月,而实际上它是按照小时计费的,从你部署之后开始计费,$5/月 ≈ $0.00694/小时,你部署完毕再删掉,这段时间的费用很低,可以忽略不计,一般来说,日本和新加坡的比较快一点,也有人说日本和新加坡服务器的端口封得比较多,容易搭建失败,具体可以自己测试一下,还有就是,只有部分地区的服务器有$2.5/月、$3.5/月的套餐,其中$2.5/月的只支持 IPv6,可以根据自己情况选择,最后操作系统建议选择 CentOS 7 x64 的,后面还有个 Enable IPv6 的选项,对 IPv6 有需求的话可以勾上,其他选项就可以不用管了。


03

04

部署成功后,点 Server Details 可以看到服务器的详细信息,其中有 IP、用户名、密码等信息,后面搭建 SSR 的时候会用到,此时你可以 ping 一下你的服务器 IP,如果 ping 不通的话,可以删掉再重新开一个服务器。


05

搭建 SSR

我们购买的是虚拟的服务器,因此需要工具远程连接到 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 ~]# 字样表示连接成功。


06

07

08

09

10

连接成功后执行以下命令开始安装 SSR:

1
wget --no-check-certificate https://freed.ga/github/shadowsocksR.sh; bash shadowsocksR.sh

如果提示 wget :command not found,可先执行 yum -y install wget,再执行上述命令即可。

执行完毕后会让你设置 SSR 连接密码和端口,然后按任意键开始搭建。


11

搭建成功后会显示你服务器 IP,端口,连接密码,协议等信息,这些信息要记住,后面使用 ShadowsocksR 的时候要用到。


12

安装锐速

由于我们购买的服务器位于国外,如果遇到上网高峰期,速度就会变慢,而锐速就是一款专业的连接加速器,可以充分利用服务器带宽,提升带宽吞吐量,其他还有类似的程序如 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

如下图所示表示内核更换完毕,此时已经断开与服务器的连接,我们需要重新连接到服务器,再执行后面的操作:


13

重新连接到服务器后,再执行以下命令:

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

然后一直回车即可,系统会自动安装锐速。


14

15

出现以下信息表示安装成功:


16

使用 SSR

常见的工具有 ShadowsocksR、SSTap(原本是个游戏加速器,现在已经停止维护,但 GitHub 上仍然可以找到)等。
Shadowsocks 官网:https://shadowsocks.org/
ShadowsocksR 下载地址:https://github.com/Anankke/SSRR-Windows
SSTap GitHub 地址:https://github.com/FQrabbit/SSTap-Rule

不管什么工具,用法都是一样的,添加一个新的代理服务器,服务器 IP、端口、密码、加密方式等等这些信息保持一致就行了。然后就可以愉快地科学上网了!


17

18

多端口配置

经过以上步骤我们就可以科学上网了,但是目前为止只有一个端口,只能一个人用,那么如何实现多个端口多人使用呢?事实上端口、密码等信息是储存在一个叫做 shadowsocks.json 文件里的,如果要添加端口或者更改密码,只需要修改此文件即可。

连接到自己的 VPS,输入以下命令,使用 vim 编辑文件:vi /etc/shadowsocks.json

原文件内容大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"server": "0.0.0.0",
"server_port": 8686,
"server_ipv6": "::",
"local_address": "127.0.0.1",
"local_port": 1081,
"password":"SSR12345",
"timeout": 120,
"udp_timeout": 60,
"method": "aes-256-cfb",
"protocol": "auth_sha1_v4_compatible",
"protocol_param": "",
"obfs": "http_simple_compatible",
"obfs_param": "",
"dns_ipv6": false,
"connect_verbose_info": 1,
"redirect": "",
"fast_open": false,
"workers": 1
}

增加端口,我们将其修改为如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"server": "0.0.0.0",
"server_ipv6": "::",
"local_address": "127.0.0.1",
"local_port": 1081,
"port_password":
{
"8686":"SSR1",
"8687":"SSR2",
"8688":"SSR3",
"8689":"SSR4",
"8690":"SSR5"
},
"timeout": 120,
"udp_timeout": 60,
"method": "aes-256-cfb",
"protocol": "auth_sha1_v4_compatible",
"protocol_param": "",
"obfs": "http_simple_compatible",
"obfs_param": "",
"dns_ipv6": false,
"connect_verbose_info": 1,
"redirect": "",
"fast_open": false,
"workers": 1
}

也就是删除原来的 server_portpassword 这两项,然后增加 port_password 这一项,前面是端口号,后面是密码,注意不要把格式改错了!!!修改完毕并保存!!!

接下来配置一下防火墙,同样的,输入以下命令,用 vim 编辑文件:vi /etc/firewalld/zones/public.xml

初始的防火墙只开放了最初配置 SSR 默认的那个端口,现在需要我们手动加上那几个新加的端口,注意:一个端口需要复制两行,一行是 tcp,一行是 udp。

原文件内容大概如下:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<zone>
<short>Public</short>
<service name="dhcpv6-client"/>
<service name="ssh"/>
<port protocol="tcp" port="8686"/>
<port protocol="udp" port="8686"/>
</zone>

修改后的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<zone>
<short>Public</short>
<service name="dhcpv6-client"/>
<service name="ssh"/>
<port protocol="tcp" port="8686"/>
<port protocol="udp" port="8686"/>
<port protocol="tcp" port="8687"/>
<port protocol="udp" port="8687"/>
<port protocol="tcp" port="8688"/>
<port protocol="udp" port="8688"/>
<port protocol="tcp" port="8689"/>
<port protocol="udp" port="8689"/>
<port protocol="tcp" port="8690"/>
<port protocol="udp" port="8690"/>
</zone>

修改完毕并保存,最后重启一下 shadowsocks,然后重新载入防火墙即可,两条命令如下:

1
/etc/init.d/shadowsocks restart
1
firewall-cmd –reload

完成之后,我们新加的这几个端口就可以使用了

另外还可以将配置转换成我们常见的链接形式,如:ss://xxxxxssr://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

]]>
<p>俗话说得好:预先善其事,必先利其器,作为一个程序员,经常会用到 GitHub、Google、Stack Overflow 啥的,由于国内政策原因,想要访问国外网站就得科学上网,最常见的工具就是 ShadowsocksR,又被称为酸酸乳、SSR、小飞机,目前市面上有很多很多的机场,价格也不是很高,完全可以订阅别人的,但是订阅别人的,数据安全没有保障,有可能你的浏览历史啥的别人都能掌握,别人也有随时跑路的可能,总之,只有完全属于自己的东西才是最香的!</p>
2019年总结【跨越今天,更不平凡】 https://www.itrhx.com/2019/12/31/A60-2019-summary/ 2019-12-31T15:14:59.983Z 2019-12-31T15:53:21.779Z
1

还记得小时候写作文,畅想2020会怎样怎样,光阴似箭,2020真的来了,度过了艰难的考试周,抽了个晚上,回想了一下,决定写一写总结吧,似乎以前都没写过呢,那干脆连带2017、2018也写写吧,重点写一写2019的,以后争取每年都做一下总结。


【2017】

2017年高三,上半年就不用说了,所有高三考生都一个样吧,下半年考进了武汉的某二本院校,软件工程专业,现在回想起来,当时把时间浪费得太多了,最开始加了一个部门,后来退了(事实上啥也学不到,浪费时间 ),然后除了完成学校的课程以外,其他啥也没搞,剩下的时间基本上全拿来骑车了,从高一开始就热爱单车运动,刚上大学肯定得放飞自我了,没课的时候就天天和学长到处跑,都快把武汉跑了个遍了,当时还定了个计划,大学四年骑车去一次西藏或者青海湖,其他的什么都没想,也没有对以后具体干哪方面做过规划,这一年收获最多的应该就是路上的风景了。


2

【2018】

2018上半年,大一下学期,学习方面就过了个英语四级,然后依旧热衷于我的单车,暑假的时候疯狂了一把,7天干了700多公里,从学校骑回家了,那个时候正是热的时候,白天基本上在三十度,从武汉往西边走,后面全是爬山,上山爬不动,下山刹不住,路上也遇到了不少牛逼人物,有徒步西藏的,有环游中国的,直播平台有好几十万粉丝的……遇到的人都很善良,很硬汉,这次经历从某种程度上来说也是一次成长吧,一次很有意义的骑行。

下半年,也就是大二开始,才慢慢开始重视专业知识的学习,大二上学期搭建了个人博客,开始尝试写博客,其实就是把博客当做笔记吧,记性不好,学了的东西容易忘记,忘记了可以经常翻自己博客再复习复习,自己踩过的坑也记录记录,后来没想到有些文章访问量还挺高的,在博客搭建方面也帮到了一些网友,最重要的是结识了不少博友,有各行各业的大佬,下半年也定了方向,开始专注Python的学习,从此开始慢慢熬夜,也渐渐地不怎么出去骑车了。


3

4

【2019】

2019 总的来说,还比较满意吧,主要是感觉过得很充实,大三基本上每天一整天都是上机课,没有太多时间搞自己的,自己倾向于Python、网络爬虫、数据分析方面,然而这些课程学校都没有,每天晚上以及周六周日都是自己在学,找了不少视频在看,有时候感觉自己还是差点火候,感觉一个简单的东西人家看一遍就会,但是我要看好几遍,不管怎样,我还是相信勤能补拙的。

【学习方面】
  • [√] 通过软考中级软件设计师
  • [√] 成为入党积极分子
  • [√] 学校大课基金结题
  • 英语六级未通过
  • 国家专利未通过
【看完或者大部分看完的书籍】
  • [√] 《软件设计师考试》
  • [√] 《Python 编程从入门到实践》
  • [√] 《Python 编程从零基础到项目实战》
  • [√] 《Python3 网络爬虫开发实战》
  • [√] 《Python 网络爬虫从入门到实践》
  • [√] 《精通 Python 爬虫框架 Scrapy》
  • [√] 《Python 程序员面试宝典(算法+数据结构)》
  • [√] 《Selenium 自动化测试 — 基于 Python 语言》
  • [√] 《重构,改善既有代码的设计》
【生活方面】

暑假受家族前辈的邀请,为整个姓氏家族编写族谱,感觉这是今年收获最大的一件事情吧,当时背着电脑跟着前辈下乡,挨家挨户统计资料,纯手工录入电脑(感觉那是我活了二十年打字打得最多的一个月,祖宗十八代都搞清楚了),最后排版打印成书,一个月下来感受到了信息化时代和传统文化的碰撞,见了很多古书,古迹,当然还领略到了古繁体字的魅力,前辈一路上给我讲述了很多书本上学不到的东西,一段很有意义的体验,感触颇深。

个人爱好上面,今年就基本上没有骑车了,没有经常骑车,开学骑了两次就跟不上别人了,后面就洗干净用布遮起来放在寝室了,按照目前情况来看,多半是要“退役”了,不知道何时才会又一次踩上脚踏,不过偶尔还是在抖音上刷刷关注的单车大佬,看看别人的视频,看到友链小伙伴 Shan San 在今年总结也写了他一年没有跳舞了,抛弃了曾经热爱的 Breaking,真的是深有感触啊。

有个遗憾就是大一的愿望实现不了了,恐怕大学四年也不会去西藏或者青海湖了,此处放一个到目前为止的骑行数据,以此纪念一下我的单车生涯吧。


5
【技术交流&实践】

自从搭建了博客之后,认识了不少大佬,经常会去大佬博客逛逛,涨涨知识

截止目前,个人博客 PV:4万+,UV:1万+,知乎:400+赞同,CSDN:43万+访问量,400+赞同

此外今年第一次为开源做了一点儿微不足道的贡献,为 Hexo 博客主题 Material X 添加了文章字数统计和阅读时长的功能,提交了人生当中第一个 PR。第一次嘛,还是值得纪念一下的。


6

我 GitHub 上虽然有一些小绿点,但是很大一部分都是推送的博客相关的东西,剩下的有几个仓库也就是 Python 相关的了,一些实战的代码放在了上面,很多时候是拿 GitHub 围观一些牛逼代码或者资源,还需要努力学习啊!


7

8

实战方面,爬虫自己也爬了很多网站,遇到一些反爬网站还不能解决,也刷了一些 Checkio 上面的题,做了题,和其他大佬相比才会发现自己的代码水平有多低,最直接的感受就是我用了很多行代码,而大神一行代码就解决了,只能说自己的水平还有很大的增进空间,新的一年继续努力吧!


9

【2020】

1024 + 996 = 2020,2020注定是不平凡的一年,定下目标,努力实现,只谈技术,莫问前程!

【计划目标】
  • 4月蓝桥杯拿奖
  • 5月通过软考高级信息系统项目管理师
  • 6月通过英语六级
  • 坚持记笔记、写博客
  • 学习 JavaScript 逆向
  • 研究网站常用反爬策略,掌握反反爬虫技术
  • 掌握两到三个主流爬虫框架
  • 加深 Python 算法和数据结构的学习
  • 学习 Python 数据可视化和数据分析
  • 做一个 Python 相关的优秀开源项目(爬虫类最好)
  • 向优秀爬虫工程师方向迈进
  • 参加 PyCon China 2020
【计划要看的书籍】
  • 《JavaScript 从入门到精通》
  • 《Python3 反爬虫原理与绕过实战》
  • 《Python 数据可视化编程实战》
  • 《Python 数据可视化之 matplotlib 实践》
  • 《Python 数据可视化之 matplotlib 精进》
  • 《基于 Python的大数据分析基础及实战》
1
2
3
>>> pip uninstall 2019
>>> pip install 2020
>>> print('Live a good life, write some good code !!!')
]]>
<fancybox><br><img src="https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A60/1.png" alt="1"><br></fancybox>
Python3 爬虫实战 — 瓜子全国二手车 https://www.itrhx.com/2019/11/15/A59-pyspider-guazi/ 2019-11-14T16:10:55.649Z 2019-12-29T07:14:02.938Z

爬取时间: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


【1x00】提取所有二手车详情页URL

分析页面,按照习惯,最开始在 headers 里面只加入 User-Agent 字段,向主页发送请求,然而返回的东西并不是主页真正的源码,因此我们加入 Cookie,再次发起请求,即可得到真实数据。

获取 Cookie:打开浏览器访问网站,打开开发工具,切换到 Network 选项卡,筛选 Doc 文件,在 Request Headers 里可以看到 Cookie 值。

注意在爬取瓜子二手车的时候,User-Agent 与 Cookie 要对应一致,也就是直接复制 Request Headers 里的 User-Agent 和 Cookie,不要自己定义一个 User-Agent,不然有可能获取不到信息!
01

分析页面,请求地址为: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 必须要有 Cookie 和 User-Agent,且两者必须对应(用浏览器访问网站后控制台里面复制)
headers = {
'Cookie': 'uuid=06ce7520-ebd1-45bc-f41f-a95f2c9b2283; ganji_uuid=7044571161649671972745; lg=1; clueSourceCode=%2A%2300; user_city_id=-1; sessionid=fefbd4f8-0a06-4e8a-dc49-8856e1a02a07; Hm_lvt_936a6d5df3f3d309bda39e92da3dd52f=1573469368,1573541270,1573541964,1573715863; close_finance_popup=2019-11-14; cainfo=%7B%22ca_a%22%3A%22-%22%2C%22ca_b%22%3A%22-%22%2C%22ca_s%22%3A%22seo_baidu%22%2C%22ca_n%22%3A%22default%22%2C%22ca_medium%22%3A%22-%22%2C%22ca_term%22%3A%22-%22%2C%22ca_content%22%3A%22-%22%2C%22ca_campaign%22%3A%22-%22%2C%22ca_kw%22%3A%22-%22%2C%22ca_i%22%3A%22-%22%2C%22scode%22%3A%22-%22%2C%22keyword%22%3A%22-%22%2C%22ca_keywordid%22%3A%22-%22%2C%22display_finance_flag%22%3A%22-%22%2C%22platform%22%3A%221%22%2C%22version%22%3A1%2C%22client_ab%22%3A%22-%22%2C%22guid%22%3A%2206ce7520-ebd1-45bc-f41f-a95f2c9b2283%22%2C%22ca_city%22%3A%22wh%22%2C%22sessionid%22%3A%22fefbd4f8-0a06-4e8a-dc49-8856e1a02a07%22%7D; _gl_tracker=%7B%22ca_source%22%3A%22-%22%2C%22ca_name%22%3A%22-%22%2C%22ca_kw%22%3A%22-%22%2C%22ca_id%22%3A%22-%22%2C%22ca_s%22%3A%22self%22%2C%22ca_n%22%3A%22-%22%2C%22ca_i%22%3A%22-%22%2C%22sid%22%3A56473912809%7D; cityDomain=www; preTime=%7B%22last%22%3A1573720945%2C%22this%22%3A1573469364%2C%22pre%22%3A1573469364%7D; Hm_lpvt_936a6d5df3f3d309bda39e92da3dd52f=1573720946; rfnl=https://www.guazi.com/www/chevrolet/i2c-1r18/; antipas=675i0t513a7447M2L9y418Qq869',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36'
}


# 获取所有二手车详情页URL
def parse_index():
response = requests.get(url=url, headers=headers)
tree = etree.HTML(response.text)
url_list = tree.xpath('//li/a[@class="car-a"]/@href')
# print(len(url_list))
return url_list


if __name__ == '__main__':
for i in range(1, 51):
url = 'https://www.guazi.com/www/buy/o%sc-1/' % i
detail_urls = parse_index()

【2x00】获取二手车详细信息并保存图片

前面的第一步我们已经获取到了二手车详情页的 URL,现在定义一个 parse_detail() 函数,向其中循环传入每一条 URL,利用 Xpath 语法匹配每一条信息,所有信息包含:标题、二手车价格、新车指导价、车主、上牌时间、表显里程、上牌地、排放标准、变速箱、排量、过户次数、看车地点、年检到期、交强险、商业险到期

其中有部分信息可能包含空格,可以用 strip() 方法将其去掉。

需要注意的是,上牌地对应的是一个 class="three"li 标签,有些二手车没有上牌地信息,匹配的结果将是空,在数据储存时就有可能出现数组越界的错误信息,所以这里可以加一个判断,如果没有上牌地信息,可以将其赋值为:未知。

保存车辆图片时,为了节省时间和空间,避免频繁爬取被封,所以只保存第一张图片,同样利用 Xpath 匹配到第一张图片的地址,以标题为图片的名称,定义储存路径后,以二进制形式保存图片。

最后整个函数返回的是一个列表 data,这个列表包含每辆二手车的所有信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# 获取二手车详细信息
def parse_detail(content):
detail_response = requests.get(url=content, headers=headers)
tree = etree.HTML(detail_response.text)

# 标题
title = tree.xpath('//h2[@class="titlebox"]/text()')
# 移除字符串头尾空格
title = [t.strip() for t in title]
# 匹配到两个元素,只取其中一个为标题
title = title[:1]
# print(title)

# 价格
price_old = tree.xpath('//span[@class="pricestype"]/text()')
# 移除字符串头尾空格
price_old = [p.strip() for p in price_old]
# 加入单位
price_old = [''.join(price_old + ['万'])]
# print(price_old)

# 新车指导价
price_new = tree.xpath('//span[@class="newcarprice"]/text()')
# 移除字符串头尾空格
price_new = [p.strip() for p in price_new]
# 对字符串进行切片,只取数字多少万
price_new = ['¥' + price_new[0].split('价')[1]]
# print(price_new)

# 车主
owner = tree.xpath('//dl/dt/span/text()')
owner = [owner[0].replace('车主:', '')]
# print(owner)

# 上牌时间
spsj = tree.xpath('//li[@class="one"]/div/text()')
# print(spsj)

# 表显里程
bxlc = tree.xpath('//li[@class="two"]/div/text()')
# print(bxlc)

# 上牌地
spd = tree.xpath('//li[@class="three"]/div/text()')
# 某些二手车没有上牌地,没有的将其赋值为:未知
if len(spd) == 0:
spd = ['未知']
# print(spd)

# 排放标准
pfbz = tree.xpath('//li[@class="four"]/div/text()')
pfbz = pfbz[:1]
# print(pfbz)

# 变速箱
bsx = tree.xpath('//li[@class="five"]/div/text()')
# print(bsx)

# 排量
pl = tree.xpath('//li[@class="six"]/div/text()')
# print(pl)

# 过户次数
ghcs = tree.xpath('//li[@class="seven"]/div/text()')
ghcs = [g.strip() for g in ghcs]
ghcs = ghcs[:1]
# print(ghcs)

# 看车地点
kcdd = tree.xpath('//li[@class="eight"]/div/text()')
# print(kcdd)

# 年检到期
njdq = tree.xpath('//li[@class="nine"]/div/text()')
# print(njdq)

# 交强险
jqx = tree.xpath('//li[@class="ten"]/div/text()')
# print(jqx)

# 商业险到期
syxdq = tree.xpath('//li[@class="last"]/div/text()')
syxdq = [s.strip() for s in syxdq]
syxdq = syxdq[:1]
# print(syxdq)

# 保存车辆图片
# 获取图片地址
pic_url = tree.xpath('//li[@class="js-bigpic"]/img/@data-src')[0]
pic_response = requests.get(pic_url)
# 定义图片名称以及保存的文件夹
pic_name = title[0] + '.jpg'
dir_name = 'guazi_pic'
# 如果没有该文件夹则创建该文件夹
if not os.path.exists(dir_name):
os.mkdir(dir_name)
# 定义储存路径
pic_path = dir_name + '/' + pic_name
with open(pic_path, "wb")as f:
f.write(pic_response.content)

# 将每辆二手车的所有信息合并为一个列表
data = title + price_old + price_new + owner + spsj + bxlc + spd + pfbz + bsx + pl + ghcs + kcdd + njdq + jqx + syxdq
return data


if __name__ == '__main__':
for i in range(1, 51):
url = 'https://www.guazi.com/www/buy/o%sc-1/' % i
detail_urls = parse_index()
for detail_url in detail_urls:
car_url = 'https://www.guazi.com' + detail_url
car_data = parse_detail(car_url)

【3x00】将数据储存到 MongoDB

定义数据储存函数 save_data()

使用 MongoClient() 方法,向其传入地址参数 host 和 端口参数 port,指定数据库为 guazi,集合为 esc

传入第二步 parse_detail() 函数返回的二手车信息的列表,依次读取其中的元素,每一个元素对应相应的信息名称

最后调用 insert_one() 方法,每次插入一辆二手车的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 将数据储存到 MongoDB
def save_data(data):
client = pymongo.MongoClient(host='localhost', port=27017)
db = client.guazi
collection = db.esc
esc = {
'标题': data[0],
'二手车价格': data[1],
'新车指导价': data[2],
'车主': data[3],
'上牌时间': data[4],
'表显里程': data[5],
'上牌地': data[6],
'排放标准': data[7],
'变速箱': data[8],
'排量': data[9],
'过户次数': data[10],
'看车地点': data[11],
'年检到期': data[12],
'交强险': data[13],
'商业险到期': data[14]
}
collection.insert_one(esc)


if __name__ == '__main__':
for i in range(1, 51):
url = 'https://www.guazi.com/www/buy/o%sc-1/' % i
detail_urls = parse_index()
for detail_url in detail_urls:
car_url = 'https://www.guazi.com' + detail_url
car_data = parse_detail(car_url)
save_data(car_data)
# 在3-10秒之间随机暂停
time.sleep(random.randint(3, 10))
time.sleep(random.randint(5, 60))
print('所有数据爬取完毕!')

【4x00】完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# =============================================
# --*-- coding: utf-8 --*--
# @Time : 2019-11-14
# @Author : TRHX
# @Blog : www.itrhx.com
# @CSDN : https://blog.csdn.net/qq_36759224
# @FileName: guazi.py
# @Software: PyCharm
# =============================================

from lxml import etree
import requests
import pymongo
import time
import random
import os

# 必须要有 Cookie 和 User-Agent,且两者必须对应(用浏览器访问网站后控制台里面复制)
headers = {
'Cookie': 'uuid=06ce7520-ebd1-45bc-f41f-a95f2c9b2283; ganji_uuid=7044571161649671972745; lg=1; clueSourceCode=%2A%2300; user_city_id=-1; sessionid=fefbd4f8-0a06-4e8a-dc49-8856e1a02a07; Hm_lvt_936a6d5df3f3d309bda39e92da3dd52f=1573469368,1573541270,1573541964,1573715863; close_finance_popup=2019-11-14; cainfo=%7B%22ca_a%22%3A%22-%22%2C%22ca_b%22%3A%22-%22%2C%22ca_s%22%3A%22seo_baidu%22%2C%22ca_n%22%3A%22default%22%2C%22ca_medium%22%3A%22-%22%2C%22ca_term%22%3A%22-%22%2C%22ca_content%22%3A%22-%22%2C%22ca_campaign%22%3A%22-%22%2C%22ca_kw%22%3A%22-%22%2C%22ca_i%22%3A%22-%22%2C%22scode%22%3A%22-%22%2C%22keyword%22%3A%22-%22%2C%22ca_keywordid%22%3A%22-%22%2C%22display_finance_flag%22%3A%22-%22%2C%22platform%22%3A%221%22%2C%22version%22%3A1%2C%22client_ab%22%3A%22-%22%2C%22guid%22%3A%2206ce7520-ebd1-45bc-f41f-a95f2c9b2283%22%2C%22ca_city%22%3A%22wh%22%2C%22sessionid%22%3A%22fefbd4f8-0a06-4e8a-dc49-8856e1a02a07%22%7D; _gl_tracker=%7B%22ca_source%22%3A%22-%22%2C%22ca_name%22%3A%22-%22%2C%22ca_kw%22%3A%22-%22%2C%22ca_id%22%3A%22-%22%2C%22ca_s%22%3A%22self%22%2C%22ca_n%22%3A%22-%22%2C%22ca_i%22%3A%22-%22%2C%22sid%22%3A56473912809%7D; cityDomain=www; preTime=%7B%22last%22%3A1573720945%2C%22this%22%3A1573469364%2C%22pre%22%3A1573469364%7D; Hm_lpvt_936a6d5df3f3d309bda39e92da3dd52f=1573720946; rfnl=https://www.guazi.com/www/chevrolet/i2c-1r18/; antipas=675i0t513a7447M2L9y418Qq869',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36'
}


# 获取所有二手车详情页URL
def parse_index():
response = requests.get(url=url, headers=headers)
tree = etree.HTML(response.text)
url_list = tree.xpath('//li/a[@class="car-a"]/@href')
# print(len(url_list))
return url_list


# 获取二手车详细信息
def parse_detail(content):
detail_response = requests.get(url=content, headers=headers)
tree = etree.HTML(detail_response.text)

# 标题
title = tree.xpath('//h2[@class="titlebox"]/text()')
# 移除字符串头尾空格
title = [t.strip() for t in title]
# 匹配到两个元素,只取其中一个为标题
title = title[:1]
# print(title)

# 价格
price_old = tree.xpath('//span[@class="pricestype"]/text()')
# 移除字符串头尾空格
price_old = [p.strip() for p in price_old]
# 加入单位
price_old = [''.join(price_old + ['万'])]
# print(price_old)

# 新车指导价
price_new = tree.xpath('//span[@class="newcarprice"]/text()')
# 移除字符串头尾空格
price_new = [p.strip() for p in price_new]
# 对字符串进行切片,只取数字多少万
price_new = ['¥' + price_new[0].split('价')[1]]
# print(price_new)

# 车主
owner = tree.xpath('//dl/dt/span/text()')
owner = [owner[0].replace('车主:', '')]
# print(owner)

# 上牌时间
spsj = tree.xpath('//li[@class="one"]/div/text()')
# print(spsj)

# 表显里程
bxlc = tree.xpath('//li[@class="two"]/div/text()')
# print(bxlc)

# 上牌地
spd = tree.xpath('//li[@class="three"]/div/text()')
# 某些二手车没有上牌地,没有的将其赋值为:未知
if len(spd) == 0:
spd = ['未知']
# print(spd)

# 排放标准
pfbz = tree.xpath('//li[@class="four"]/div/text()')
pfbz = pfbz[:1]
# print(pfbz)

# 变速箱
bsx = tree.xpath('//li[@class="five"]/div/text()')
# print(bsx)

# 排量
pl = tree.xpath('//li[@class="six"]/div/text()')
# print(pl)

# 过户次数
ghcs = tree.xpath('//li[@class="seven"]/div/text()')
ghcs = [g.strip() for g in ghcs]
ghcs = ghcs[:1]
# print(ghcs)

# 看车地点
kcdd = tree.xpath('//li[@class="eight"]/div/text()')
# print(kcdd)

# 年检到期
njdq = tree.xpath('//li[@class="nine"]/div/text()')
# print(njdq)

# 交强险
jqx = tree.xpath('//li[@class="ten"]/div/text()')
# print(jqx)

# 商业险到期
syxdq = tree.xpath('//li[@class="last"]/div/text()')
syxdq = [s.strip() for s in syxdq]
syxdq = syxdq[:1]
# print(syxdq)

# 保存车辆图片
# 获取图片地址
pic_url = tree.xpath('//li[@class="js-bigpic"]/img/@data-src')[0]
pic_response = requests.get(pic_url)
# 定义图片名称以及保存的文件夹
pic_name = title[0] + '.jpg'
dir_name = 'guazi_pic'
# 如果没有该文件夹则创建该文件夹
if not os.path.exists(dir_name):
os.mkdir(dir_name)
# 定义储存路径
pic_path = dir_name + '/' + pic_name
with open(pic_path, "wb")as f:
f.write(pic_response.content)

# 将每辆二手车的所有信息合并为一个列表
data = title + price_old + price_new + owner + spsj + bxlc + spd + pfbz + bsx + pl + ghcs + kcdd + njdq + jqx + syxdq
return data


# 将数据储存到 MongoDB
def save_data(data):
client = pymongo.MongoClient(host='localhost', port=27017)
db = client.guazi
collection = db.esc
esc = {
'标题': data[0],
'二手车价格': data[1],
'新车指导价': data[2],
'车主': data[3],
'上牌时间': data[4],
'表显里程': data[5],
'上牌地': data[6],
'排放标准': data[7],
'变速箱': data[8],
'排量': data[9],
'过户次数': data[10],
'看车地点': data[11],
'年检到期': data[12],
'交强险': data[13],
'商业险到期': data[14]
}
collection.insert_one(esc)


if __name__ == '__main__':
for i in range(1, 51):
num = 0
print('正在爬取第' + str(i) + '页数据...')
url = 'https://www.guazi.com/www/buy/o%sc-1/' % i
detail_urls = parse_index()
for detail_url in detail_urls:
car_url = 'https://www.guazi.com' + detail_url
car_data = parse_detail(car_url)
save_data(car_data)
num += 1
print('第' + str(num) + '条数据爬取完毕!')
# 在3-10秒之间随机暂停
time.sleep(random.randint(3, 10))
print('第' + str(i) + '页数据爬取完毕!')
print('=====================')
time.sleep(random.randint(5, 60))
print('所有数据爬取完毕!')

【5x00】数据截图

爬取的汽车图片:


02

储存到 MongoDB 的数据:


03

数据导出为 CSV 文件:


04

【6x00】程序不足的地方

Cookie 过一段时间就会失效,数据还没爬取完就失效了,导致无法继续爬取;爬取效率不高,可以考虑多线程爬取

]]>
<blockquote> <p>爬取时间:2019-11-14<br>爬取难度:★★☆☆☆☆<br>请求链接:<a href="https://www.guazi.com/www/buy/" target="_blank" rel="noopener">https://www.guazi.com/www/buy/</a><br>爬取目标:爬取瓜子全国二手车信息,包括价格、上牌时间、表显里程等;保存车辆图片<br>涉及知识:请求库 requests、解析库 lxml、Xpath 语法、数据库 MongoDB 的操作<br>完整代码:<a href="https://github.com/TRHX/Python3-Spider-Practice/tree/master/guazi" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice/tree/master/guazi</a><br>其他爬虫实战代码合集(持续更新):<a href="https://github.com/TRHX/Python3-Spider-Practice" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice</a><br>爬虫实战专栏(持续更新):<a href="https://itrhx.blog.csdn.net/article/category/9351278" target="_blank" rel="noopener">https://itrhx.blog.csdn.net/article/category/9351278</a></p> </blockquote> <hr>
Python3 爬虫实战 — 58同城武汉出租房【加密字体对抗】 https://www.itrhx.com/2019/10/21/A58-pyspider-58tongcheng/ 2019-10-21T13:22:53.980Z 2019-10-21T13:32:53.413Z

爬取时间: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


【1x00】加密字体攻克思路

F12 打开调试模板,通过页面分析,可以观察到,网站里面凡是涉及到有数字的地方,都是显示为乱码,这种情况就是字体加密了,那么是通过什么手段实现字体加密的呢?

CSS 中有一个 @font-face 规则,它允许为网页指定在线字体,也就是说可以引入自定义字体,这个规则本意是用来消除对电脑字体的依赖,现在不少网站也利用这个规则来实现反爬

右侧可以看到网站用的字体,其他的都是常见的微软雅黑,宋体等,但是有一个特殊的:fangchan-secret ,不难看出这应该就是58同城的自定义字体了


01

我们通过控制台看到的乱码事实上是由于 unicode 编码导致,查看网页源代码,我们才能看到他真正的编码信息


02

要攻克加密字体,那么我们肯定要分析他的字体文件了,先想办法得到他的加密字体文件,同样查看源代码,在源代码中搜索 fangchan-secret 的字体信息


03

选中的蓝色部分就是 base64 编码的加密字体字符串了,我们将其解码成二进制编码,写进 .woff 的字体文件,这个过程可以通过以下代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import base64

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}

url = 'https://wh.58.com/chuzu/'

response = requests.get(url=url, headers=headers)
# 匹配 base64 编码的加密字体字符串
base64_string = response.text.split("base64,")[1].split("'")[0].strip()
# 将 base64 编码的字体字符串解码成二进制编码
bin_data = base64.decodebytes(base64_string.encode())
# 保存为字体文件
with open('58font.woff', 'wb') as f:
f.write(bin_data)

得到字体文件后,我们可以通过 FontCreator 这个软件来看看字体对应的编码是什么:


04

观察我们在网页源代码中看到的编码:类似于 &#x9fa4;&#x9f92;

对比字体文件对应的编码:类似于 uni9FA4nui9F92

可以看到除了前面三个字符不一样以外,后面的字符都是一样的,只不过英文大小写有所差异

现在我们可能会想到,直接把编码替换成对应的数字不就OK了?然而并没有这么简单

尝试刷新一下网页,可以观察到 base64 编码的加密字体字符串会改变,也就是说编码和数字并不是一一对应的,再次获取几个字体文件,通过对比就可以看出来


05

可以看到,虽然每次数字对应的编码都不一样,但是编码总是这10个,是不变的,那么编码与数字之间肯定存在某种对应关系,,我们可以将字体文件转换为 xml 文件来观察其中的对应关系,改进原来的代码即可实现转换功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import base64
from fontTools.ttLib import TTFont

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}

url = 'https://wh.58.com/chuzu/'

response = requests.get(url=url, headers=headers)
# 匹配 base64 编码的加密字体字符串
base64_string = response.text.split("base64,")[1].split("'")[0].strip()
# 将 base64 编码的字体字符串解码成二进制编码
bin_data = base64.decodebytes(base64_string.encode())
# 保存为字体文件
with open('58font.woff', 'wb') as f:
f.write(bin_data)
# 获取字体文件,将其转换为xml文件
font = TTFont('58font.woff')
font.saveXML('58font.xml')

打开 58font.xml 文件并分析,在 <cmap> 标签内可以看到熟悉的类似于 0x94760x958f 的编码,其后四位字符恰好是网页字体的加密编码,可以看到每一个编码后面都对应了一个 glyph 开头的编码

将其与 58font.woff 文件对比,可以看到 code 为 0x958f 这个编码对应的是数字 3,对应的 name 编码是 glyph00004


06

我们再次获取一个字体文件作为对比分析


07

依然是 0x958f 这个编码,两次对应的 name 分别是 glyph00004glyph00007,两次对应的数字分别是 36,那么结论就来了,每次发送请求,code 对应的 name 会随机发生变化,而 name 对应的数字不会发生变化,glyph00001 对应数字 0glyph00002 对应数字 1,以此类推

那么以 glyph 开头的编码是如何对应相应的数字的呢?在 xml 文件里面,每个编码都有一个 TTGlyph 的标签,标签里面是一行一行的类似于 x,y 坐标的东西,这个其实就是用来绘制字体的,用 matplotlib 根据坐标画个图,就可以看到是一个数字


08

此时,我们就知道了编码与数字的对应关系,下一步,我们可以查找 xml 文件里,编码对应的 name 的值,也就是以 glyph 开头的编码,然后返回其对应的数字,再替换掉网页源代码里的编码,就能成功获取到我们需要的信息了!

总结一下攻克加密字体的大致思路:

  • 分析网页,找到对应的加密字体文件

  • 如果引用的加密字体是一个 base64 编码的字符串,则需要转换成二进制并保存到 woff 字体文件中

  • 将字体文件转换成 xml 文件

  • 用 FontCreator 软件观察字体文件,结合 xml 文件,分析其编码与真实字体的关系

  • 搞清楚编码与字体的关系后,想办法将编码替换成正常字体


【2x00】思维导图


09

【3x00】加密字体处理模块

【3x01】获取字体文件并转换为xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_font(page_url, page_num):
response = requests.get(url=page_url, headers=headers)
# 匹配 base64 编码的加密字体字符串
base64_string = response.text.split("base64,")[1].split("'")[0].strip()
# print(base64_string)
# 将 base64 编码的字体字符串解码成二进制编码
bin_data = base64.decodebytes(base64_string.encode())
# 保存为字体文件
with open('58font.woff', 'wb') as f:
f.write(bin_data)
print('第' + str(page_num) + '次访问网页,字体文件保存成功!')
# 获取字体文件,将其转换为xml文件
font = TTFont('58font.woff')
font.saveXML('58font.xml')
print('已成功将字体文件转换为xml文件!')
return response.text

由主函数传入要发送请求的 url,利用字符串的 split() 方法,匹配 base64 编码的加密字体字符串,利用 base64 模块的 base64.decodebytes() 方法,将 base64 编码的字体字符串解码成二进制编码并保存为字体文件,利用 FontTools 库,将字体文件转换为 xml 文件


【3x02】将加密字体编码与真实字体进行匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def find_font():
# 以glyph开头的编码对应的数字
glyph_list = {
'glyph00001': '0',
'glyph00002': '1',
'glyph00003': '2',
'glyph00004': '3',
'glyph00005': '4',
'glyph00006': '5',
'glyph00007': '6',
'glyph00008': '7',
'glyph00009': '8',
'glyph00010': '9'
}
# 十个加密字体编码
unicode_list = ['0x9476', '0x958f', '0x993c', '0x9a4b', '0x9e3a', '0x9ea3', '0x9f64', '0x9f92', '0x9fa4', '0x9fa5']
num_list = []
# 利用xpath语法匹配xml文件内容
font_data = etree.parse('./58font.xml')
for unicode in unicode_list:
# 依次循环查找xml文件里code对应的name
result = font_data.xpath("//cmap//map[@code='{}']/@name".format(unicode))[0]
# print(result)
# 循环字典的key,如果code对应的name与字典的key相同,则得到key对应的value
for key in glyph_list.keys():
if key == result:
num_list.append(glyph_list[key])
print('已成功找到编码所对应的数字!')
# print(num_list)
# 返回value列表
return num_list

由前面的分析,我们知道 name 的值(即以 glyph 开头的编码)对应的数字是固定的,glyph00001 对应数字 0glyph00002 对应数字 1,以此类推,所以可以将其构造成为一个字典 glyph_list

同样将十个 code(即类似于 0x9476 的加密字体编码)构造成一个列表

循环查找这十个 code 在 xml 文件里对应的 name 的值,然后将 name 的值与字典文件的 key 值进行对比,如果两者值相同,则获取这个 keyvalue 值,最终得到的列表 num_list,里面的元素就是 unicode_list 列表里面每个加密字体的真实值


【3x03】替换掉网页中所有的加密字体编码

1
2
3
4
5
def replace_font(num, page_response):
# 9476 958F 993C 9A4B 9E3A 9EA3 9F64 9F92 9FA4 9FA5
result = page_response.replace('&#x9476;', num[0]).replace('&#x958f;', num[1]).replace('&#x993c;', num[2]).replace('&#x9a4b;', num[3]).replace('&#x9e3a;', num[4]).replace('&#x9ea3;', num[5]).replace('&#x9f64;', num[6]).replace('&#x9f92;', num[7]).replace('&#x9fa4;', num[8]).replace('&#x9fa5;', num[9])
print('已成功将所有加密字体替换!')
return result

传入由上一步 find_font() 函数得到的真实字体的列表,利用 replace() 方法,依次将十个加密字体编码替换掉


【4x00】租房信息提取模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def parse_pages(pages):
num = 0
soup = BeautifulSoup(pages, 'lxml')
# 查找到包含所有租房的li标签
all_house = soup.find_all('li', class_='house-cell')
for house in all_house:
# 标题
title = house.find('a', class_='strongbox').text.strip()
# print(title)

# 价格
price = house.find('div', class_='money').text.strip()
# print(price)

# 户型和面积
layout = house.find('p', class_='room').text.replace(' ', '')
# print(layout)

# 楼盘和地址
address = house.find('p', class_='infor').text.replace(' ', '').replace('\n', '')
# print(address)

# 如果存在经纪人
if house.find('div', class_='jjr'):
agent = house.find('div', class_='jjr').text.replace(' ', '').replace('\n', '')
# 如果存在品牌公寓
elif house.find('p', class_='gongyu'):
agent = house.find('p', class_='gongyu').text.replace(' ', '').replace('\n', '')
# 如果存在个人房源
else:
agent = house.find('p', class_='geren').text.replace(' ', '').replace('\n', '')
# print(agent)

data = [title, price, layout, address, agent]
save_to_mysql(data)
num += 1
print('第' + str(num) + '条数据爬取完毕,暂停3秒!')
time.sleep(3)

利用 BeautifulSoup 解析库很容易提取到相关信息,这里要注意的是,租房信息来源分为三种:经纪人、品牌公寓和个人房源,这三个的元素节点也不一样,因此匹配的时候要注意


10

【5x00】MySQL数据储存模块

【5x01】创建MySQL数据库的表

1
2
3
4
5
6
def create_mysql_table():
db = pymysql.connect(host='localhost', user='root', password='000000', port=3306, db='58tc_spiders')
cursor = db.cursor()
sql = 'CREATE TABLE IF NOT EXISTS 58tc_data (title VARCHAR(255) NOT NULL, price VARCHAR(255) NOT NULL, layout VARCHAR(255) NOT NULL, address VARCHAR(255) NOT NULL, agent VARCHAR(255) NOT NULL)'
cursor.execute(sql)
db.close()

首先指定数据库为 58tc_spiders,需要事先使用 MySQL 语句创建,也可以通过 MySQL Workbench 手动创建

然后使用 SQL 语句创建 一个表:58tc_data,表中包含 title、price、layout、address、agent 五个字段,类型都为 varchar

此创建表的操作也可以事先手动创建,手动创建后就不需要此函数了


【5x02】将数据储存到MySQL数据库

1
2
3
4
5
6
7
8
9
10
def save_to_mysql(data):
db = pymysql.connect(host='localhost', user='root', password='000000', port=3306, db='58tc_spiders')
cursor = db.cursor()
sql = 'INSERT INTO 58tc_data(title, price, layout, address, agent) values(%s, %s, %s, %s, %s)'
try:
cursor.execute(sql, (data[0], data[1], data[2], data[3], data[4]))
db.commit()
except:
db.rollback()
db.close()

commit() 方法的作用是实现数据插入,是真正将语句提交到数据库执行的方法,使用 try except 语句实现异常处理,如果执行失败,则调用 rollback() 方法执行数据回滚,保证原数据不被破坏


【6x00】完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# =============================================
# --*-- coding: utf-8 --*--
# @Time : 2019-10-21
# @Author : TRHX
# @Blog : www.itrhx.com
# @CSDN : https://blog.csdn.net/qq_36759224
# @FileName: 58tongcheng.py
# @Software: PyCharm
# =============================================

import requests
import time
import random
import base64
import pymysql
from lxml import etree
from bs4 import BeautifulSoup
from fontTools.ttLib import TTFont

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}


# 获取字体文件并转换为xml文件
def get_font(page_url, page_num):
response = requests.get(url=page_url, headers=headers)
# 匹配 base64 编码的加密字体字符串
base64_string = response.text.split("base64,")[1].split("'")[0].strip()
# print(base64_string)
# 将 base64 编码的字体字符串解码成二进制编码
bin_data = base64.decodebytes(base64_string.encode())
# 保存为字体文件
with open('58font.woff', 'wb') as f:
f.write(bin_data)
print('第' + str(page_num) + '次访问网页,字体文件保存成功!')
# 获取字体文件,将其转换为xml文件
font = TTFont('58font.woff')
font.saveXML('58font.xml')
print('已成功将字体文件转换为xml文件!')
return response.text


# 将加密字体编码与真实字体进行匹配
def find_font():
# 以glyph开头的编码对应的数字
glyph_list = {
'glyph00001': '0',
'glyph00002': '1',
'glyph00003': '2',
'glyph00004': '3',
'glyph00005': '4',
'glyph00006': '5',
'glyph00007': '6',
'glyph00008': '7',
'glyph00009': '8',
'glyph00010': '9'
}
# 十个加密字体编码
unicode_list = ['0x9476', '0x958f', '0x993c', '0x9a4b', '0x9e3a', '0x9ea3', '0x9f64', '0x9f92', '0x9fa4', '0x9fa5']
num_list = []
# 利用xpath语法匹配xml文件内容
font_data = etree.parse('./58font.xml')
for unicode in unicode_list:
# 依次循环查找xml文件里code对应的name
result = font_data.xpath("//cmap//map[@code='{}']/@name".format(unicode))[0]
# print(result)
# 循环字典的key,如果code对应的name与字典的key相同,则得到key对应的value
for key in glyph_list.keys():
if key == result:
num_list.append(glyph_list[key])
print('已成功找到编码所对应的数字!')
# print(num_list)
# 返回value列表
return num_list


# 替换掉网页中所有的加密字体编码
def replace_font(num, page_response):
# 9476 958F 993C 9A4B 9E3A 9EA3 9F64 9F92 9FA4 9FA5
result = page_response.replace('&#x9476;', num[0]).replace('&#x958f;', num[1]).replace('&#x993c;', num[2]).replace('&#x9a4b;', num[3]).replace('&#x9e3a;', num[4]).replace('&#x9ea3;', num[5]).replace('&#x9f64;', num[6]).replace('&#x9f92;', num[7]).replace('&#x9fa4;', num[8]).replace('&#x9fa5;', num[9])
print('已成功将所有加密字体替换!')
return result


# 提取租房信息
def parse_pages(pages):
num = 0
soup = BeautifulSoup(pages, 'lxml')
# 查找到包含所有租房的li标签
all_house = soup.find_all('li', class_='house-cell')
for house in all_house:
# 标题
title = house.find('a', class_='strongbox').text.strip()
# print(title)

# 价格
price = house.find('div', class_='money').text.strip()
# print(price)

# 户型和面积
layout = house.find('p', class_='room').text.replace(' ', '')
# print(layout)

# 楼盘和地址
address = house.find('p', class_='infor').text.replace(' ', '').replace('\n', '')
# print(address)

# 如果存在经纪人
if house.find('div', class_='jjr'):
agent = house.find('div', class_='jjr').text.replace(' ', '').replace('\n', '')
# 如果存在品牌公寓
elif house.find('p', class_='gongyu'):
agent = house.find('p', class_='gongyu').text.replace(' ', '').replace('\n', '')
# 如果存在个人房源
else:
agent = house.find('p', class_='geren').text.replace(' ', '').replace('\n', '')
# print(agent)

data = [title, price, layout, address, agent]
save_to_mysql(data)
num += 1
print('第' + str(num) + '条数据爬取完毕,暂停3秒!')
time.sleep(3)


# 创建MySQL数据库的表:58tc_data
def create_mysql_table():
db = pymysql.connect(host='localhost', user='root', password='000000', port=3306, db='58tc_spiders')
cursor = db.cursor()
sql = 'CREATE TABLE IF NOT EXISTS 58tc_data (title VARCHAR(255) NOT NULL, price VARCHAR(255) NOT NULL, layout VARCHAR(255) NOT NULL, address VARCHAR(255) NOT NULL, agent VARCHAR(255) NOT NULL)'
cursor.execute(sql)
db.close()


# 将数据储存到MySQL数据库
def save_to_mysql(data):
db = pymysql.connect(host='localhost', user='root', password='000000', port=3306, db='58tc_spiders')
cursor = db.cursor()
sql = 'INSERT INTO 58tc_data(title, price, layout, address, agent) values(%s, %s, %s, %s, %s)'
try:
cursor.execute(sql, (data[0], data[1], data[2], data[3], data[4]))
db.commit()
except:
db.rollback()
db.close()


if __name__ == '__main__':
create_mysql_table()
print('MySQL表58tc_data创建成功!')
for i in range(1, 71):
url = 'https://wh.58.com/chuzu/pn' + str(i) + '/'
response = get_font(url, i)
num_list = find_font()
pro_pages = replace_font(num_list, response)
parse_pages(pro_pages)
print('第' + str(i) + '页数据爬取完毕!')
time.sleep(random.randint(3, 60))
print('所有数据爬取完毕!')

【7x00】数据截图


11
]]>
<blockquote> <p>爬取时间:2019-10-21<br>爬取难度:★★★☆☆☆<br>请求链接:<a href="https://wh.58.com/chuzu/" target="_blank" rel="noopener">https://wh.58.com/chuzu/</a><br>爬取目标:58同城武汉出租房的所有信息<br>涉及知识:网站加密字体的攻克、请求库 requests、解析库 Beautiful Soup、数据库 MySQL 的操作<br>完整代码:<a href="https://github.com/TRHX/Python3-Spider-Practice/tree/master/58tongcheng" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice/tree/master/58tongcheng</a><br>其他爬虫实战代码合集(持续更新):<a href="https://github.com/TRHX/Python3-Spider-Practice" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice</a><br>爬虫实战专栏(持续更新):<a href="https://itrhx.blog.csdn.net/article/category/9351278" target="_blank" rel="noopener">https://itrhx.blog.csdn.net/article/category/9351278</a></p> </blockquote> <hr>
Python3 爬虫实战 — 模拟登陆12306【点触验证码对抗】 https://www.itrhx.com/2019/10/21/A57-pyspider-12306-login/ 2019-10-21T08:41:50.349Z 2019-10-21T13:33:47.617Z

登陆时间: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


【1x00】思维导图


01
  • 利用自动化测试工具 Selenium 直接模拟人的行为方式来完成验证

  • 发送请求,出现验证码后,剪裁并保存验证码图片

  • 选择在线打码平台,获取其API,以字节流格式发送图片

  • 打码平台人工识别验证码,返回验证码的坐标信息

  • 解析返回的坐标信息,模拟点击验证码,完成验证后点击登陆


02

【2x00】打码平台选择

关于打码平台:在线打码平台全部都是人工在线识别,准确率非常高,原理就是先将验证码图片提交给平台,平台会返回识别结果在图片中的坐标位置,然后我们再解析坐标模拟点击即可,常见的打码平台有超级鹰、云打码等,打码平台是收费的,拿超级鹰来说,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,下载下来以备后用


【3x00】初始化模块

【3x01】初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 12306账号密码
USERNAME = '155********'
PASSWORD = '***********'

# 超级鹰打码平台账号密码
CHAOJIYING_USERNAME = '*******'
CHAOJIYING_PASSWORD = '*******'

# 超级鹰打码平台软件ID
CHAOJIYING_SOFT_ID = '********'
# 验证码类型
CHAOJIYING_KIND = '9004'


class CrackTouClick():
def __init__(self):
self.url = 'https://kyfw.12306.cn/otn/resources/login.html'
# path是谷歌浏览器驱动的目录,如果已经将目录添加到系统变量,则不用设置此路径
path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe'
chrome_options = Options()
chrome_options.add_argument('--start-maximized')
self.browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
self.wait = WebDriverWait(self.browser, 20)
self.username = USERNAME
self.password = PASSWORD
self.chaojiying = ChaojiyingClient(CHAOJIYING_USERNAME, CHAOJIYING_PASSWORD, CHAOJIYING_SOFT_ID)

定义 12306 账号(USERNAME)、密码(PASSWORD)、超级鹰用户名(CHAOJIYING_USERNAME)、超级鹰登录密码(CHAOJIYING_PASSWORD)、超级鹰软件 ID(CHAOJIYING_SOFT_ID)、验证码类型(CHAOJIYING_KIND),登录页面 url ,谷歌浏览器驱动的目录(path),浏览器启动参数等,将超级鹰账号密码等相关参数传递给超级鹰 API


【3x02】账号密码输入函数

1
2
3
4
5
6
7
8
9
10
11
12
13
def get_input_element(self):
# 登录页面发送请求
self.browser.get(self.url)
# 登录页面默认是扫码登录,所以首先要点击账号登录
login = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.login-hd-account')))
login.click()
time.sleep(3)
# 查找到账号密码输入位置的元素
username = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'input#J-userName')))
password = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'input#J-password')))
# 输入账号密码
username.send_keys(self.username)
password.send_keys(self.password)

分析页面可知,登陆页面默认出现的是扫描二维码登陆,所以要先点击账号登录,找到该 CSS 元素为 login-hd-account,调用 click() 方法实现模拟点击,此时出现账号密码输入框,同样找到其 ID 分别为 J-userNameJ-password,调用 send_keys() 方法输入账号密码


【4x00】验证码处理模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def crack(self):
# 调用账号密码输入函数
self.get_input_element()
# 调用验证码图片剪裁函数
image = self.get_touclick_image()
bytes_array = BytesIO()
image.save(bytes_array, format='PNG')
# 利用超级鹰打码平台的 API PostPic() 方法把图片发送给超级鹰后台,发送的图像是字节流格式,返回的结果是一个JSON
result = self.chaojiying.PostPic(bytes_array.getvalue(), CHAOJIYING_KIND)
print(result)
# 调用验证码坐标解析函数
locations = self.get_points(result)
# 调用模拟点击验证码函数
self.touch_click_words(locations)
# 调用模拟点击登录函数
self.login()
try:
# 查找是否出现用户的姓名,若出现表示登录成功
success = self.wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, '.welcome-name'), '谭先生'))
print(success)
cc = self.browser.find_element(By.CSS_SELECTOR, '.welcome-name')
print('用户' + cc.text + '登录成功')
# 若没有出现表示登录失败,继续重试,超级鹰会返回本次识别的分值
except TimeoutException:
self.chaojiying.ReportError(result['pic_id'])
self.crack()

crack() 为验证码处理模块的主函数

调用账号密码输入函数 get_input_element(),等待账号密码输入完毕

调用验证码图片剪裁函数 get_touclick_image(),得到验证码图片

利用超级鹰打码平台的 API PostPic() 方法把图片发送给超级鹰后台,发送的图像是字节流格式,返回的结果是一个JSON,如果识别成功,典型的返回结果类似于:

1
2
{'err_no': 0, 'err_str': 'OK', 'pic_id': '6002001380949200001', 'pic_str': '132,127|56,77', 'md5': 
'1f8e1d4bef8b11484cb1f1f34299865b'}

其中,pic_str 就是识别的文字的坐标,是以字符串形式返回的,每个坐标都以 | 分隔

调用 get_points() 函数解析超级鹰识别结果

调用 touch_click_words() 函数对符合要求的图片进行点击

调用模拟点击登录函数 login(),点击登陆按钮模拟登陆

使用 try-except 语句判断是否出现了用户信息,判断依据是是否有用户姓名的出现,出现的姓名和实际姓名一致则登录成功,如果失败了就重试,超级鹰会返回该分值


【4x01】验证码图片剪裁函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def get_touclick_image(self, name='12306.png'):
# 获取验证码的位置
element = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.login-pwd-code')))
time.sleep(3)
location = element.location
size = element.size
top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size['width']
# 先对整个页面截图
screenshot = self.browser.get_screenshot_as_png()
screenshot = Image.open(BytesIO(screenshot))
# 根据验证码坐标信息,剪裁出验证码图片
captcha = screenshot.crop((left, top, right, bottom))
captcha.save(name)
return captcha

首先查找到验证码的坐标信息,先对整个页面截图,然后根据验证码坐标信息,剪裁出验证码图片

location 属性可以返回该图片对象在浏览器中的位置,坐标轴是以屏幕左上角为原点,x 轴向右递增,y 轴向下递增,size 属性可以返回该图片对象的高度和宽度,由此可以得到验证码的位置信息


【4x02】验证码坐标解析函数

1
2
3
4
5
6
def get_points(self, captcha_result):
# 超级鹰识别结果以字符串形式返回,每个坐标都以|分隔
groups = captcha_result.get('pic_str').split('|')
# 将坐标信息变成列表的形式
locations = [[int(number) for number in group.split(',')] for group in groups]
return locations

get_points() 方法将超级鹰的验证码识别结果变成列表的形式


【4x03】模拟点击验证码函数

1
2
3
4
5
6
def touch_click_words(self, locations):
element = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.login-pwd-code')))
# 循环点击正确验证码的坐标
for location in locations:
print(location)
ActionChains(self.browser).move_to_element_with_offset(element, location[0], location[1]).click().perform()

循环提取正确的验证码坐标信息,依次点击验证码


【5x00】登录模块

1
2
3
def login(self):
submit = self.wait.until(EC.element_to_be_clickable((By.ID, 'J-login')))
submit.click()

分析页面,找到登陆按钮的 ID 为 J-login,调用 click() 方法模拟点击按钮实现登录


【6x00】完整代码

【6x01】12306.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# =============================================
# --*-- coding: utf-8 --*--
# @Time : 2019-10-21
# @Author : TRHX
# @Blog : www.itrhx.com
# @CSDN : https://blog.csdn.net/qq_36759224
# @FileName: 12306.py
# @Software: PyCharm
# =============================================

import time
from io import BytesIO
from PIL import Image
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from chaojiying import ChaojiyingClient
from selenium.common.exceptions import TimeoutException

# 12306账号密码
USERNAME = '155********'
PASSWORD = '***********'

# 超级鹰打码平台账号密码
CHAOJIYING_USERNAME = '********'
CHAOJIYING_PASSWORD = '********'

# 超级鹰打码平台软件ID
CHAOJIYING_SOFT_ID = '******'
# 验证码类型
CHAOJIYING_KIND = '9004'


class CrackTouClick():
def __init__(self):
self.url = 'https://kyfw.12306.cn/otn/resources/login.html'
# path是谷歌浏览器驱动的目录,如果已经将目录添加到系统变量,则不用设置此路径
path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe'
chrome_options = Options()
chrome_options.add_argument('--start-maximized')
self.browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
self.wait = WebDriverWait(self.browser, 20)
self.username = USERNAME
self.password = PASSWORD
self.chaojiying = ChaojiyingClient(CHAOJIYING_USERNAME, CHAOJIYING_PASSWORD, CHAOJIYING_SOFT_ID)

def crack(self):
# 调用账号密码输入函数
self.get_input_element()
# 调用验证码图片剪裁函数
image = self.get_touclick_image()
bytes_array = BytesIO()
image.save(bytes_array, format='PNG')
# 利用超级鹰打码平台的 API PostPic() 方法把图片发送给超级鹰后台,发送的图像是字节流格式,返回的结果是一个JSON
result = self.chaojiying.PostPic(bytes_array.getvalue(), CHAOJIYING_KIND)
print(result)
# 调用验证码坐标解析函数
locations = self.get_points(result)
# 调用模拟点击验证码函数
self.touch_click_words(locations)
# 调用模拟点击登录函数
self.login()
try:
# 查找是否出现用户的姓名,若出现表示登录成功
success = self.wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, '.welcome-name'), '谭先生'))
print(success)
cc = self.browser.find_element(By.CSS_SELECTOR, '.welcome-name')
print('用户' + cc.text + '登录成功')
# 若没有出现表示登录失败,继续重试,超级鹰会返回本次识别的分值
except TimeoutException:
self.chaojiying.ReportError(result['pic_id'])
self.crack()

# 账号密码输入函数
def get_input_element(self):
# 登录页面发送请求
self.browser.get(self.url)
# 登录页面默认是扫码登录,所以首先要点击账号登录
login = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.login-hd-account')))
login.click()
time.sleep(3)
# 查找到账号密码输入位置的元素
username = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'input#J-userName')))
password = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'input#J-password')))
# 输入账号密码
username.send_keys(self.username)
password.send_keys(self.password)

# 验证码图片剪裁函数
def get_touclick_image(self, name='12306.png'):
# 获取验证码的位置
element = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.login-pwd-code')))
time.sleep(3)
location = element.location
size = element.size
top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size[
'width']
# 先对整个页面截图
screenshot = self.browser.get_screenshot_as_png()
screenshot = Image.open(BytesIO(screenshot))
# 根据验证码坐标信息,剪裁出验证码图片
captcha = screenshot.crop((left, top, right, bottom))
captcha.save(name)
return captcha

# 验证码坐标解析函数,分析超级鹰返回的坐标
def get_points(self, captcha_result):
# 超级鹰识别结果以字符串形式返回,每个坐标都以|分隔
groups = captcha_result.get('pic_str').split('|')
# 将坐标信息变成列表的形式
locations = [[int(number) for number in group.split(',')] for group in groups]
return locations

# 模拟点击验证码函数
def touch_click_words(self, locations):
element = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.login-pwd-code')))
# 循环点击正确验证码的坐标
for location in locations:
print(location)
ActionChains(self.browser).move_to_element_with_offset(element, location[0], location[1]).click().perform()

# 模拟点击登录函数
def login(self):
submit = self.wait.until(EC.element_to_be_clickable((By.ID, 'J-login')))
submit.click()


if __name__ == '__main__':
crack = CrackTouClick()
crack.crack()

【6x02】chaojiying.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import requests
from hashlib import md5


class ChaojiyingClient(object):
def __init__(self, username, password, soft_id):
self.username = username
password = password.encode('utf8')
self.password = md5(password).hexdigest()
self.soft_id = soft_id
self.base_params = {
'user': self.username,
'pass2': self.password,
'softid': self.soft_id,
}
self.headers = {
'Connection': 'Keep-Alive',
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
}

def PostPic(self, im, codetype):
"""
im: 图片字节
codetype: 题目类型 参考 http://www.chaojiying.com/price.html
"""
params = {
'codetype': codetype,
}
params.update(self.base_params)
files = {'userfile': ('ccc.jpg', im)}
r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files, headers=self.headers)
return r.json()

def ReportError(self, im_id):
"""
im_id:报错题目的图片ID
"""
params = {
'id': im_id,
}
params.update(self.base_params)
r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
return r.json()

【7x00】效果实现动图

最终实现效果图:(关键信息已经过打码处理)


02
]]>
<blockquote> <p>登陆时间:2019-10-21<br>实现难度:★★★☆☆☆<br>请求链接:<a href="https://kyfw.12306.cn/otn/resources/login.html" target="_blank" rel="noopener">https://kyfw.12306.cn/otn/resources/login.html</a><br>实现目标:模拟登陆中国铁路12306,攻克点触验证码<br>涉及知识:点触验证码的攻克、自动化测试工具 Selenium 的使用、对接在线打码平台<br>完整代码:<a href="https://github.com/TRHX/Python3-Spider-Practice/tree/master/12306-login" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice/tree/master/12306-login</a><br>其他爬虫实战代码合集(持续更新):<a href="https://github.com/TRHX/Python3-Spider-Practice" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice</a><br>爬虫实战专栏(持续更新):<a href="https://itrhx.blog.csdn.net/article/category/9351278" target="_blank" rel="noopener">https://itrhx.blog.csdn.net/article/category/9351278</a></p> </blockquote> <hr>
Python3 爬虫实战 — 模拟登陆哔哩哔哩【滑动验证码对抗】 https://www.itrhx.com/2019/10/21/A56-pyspider-bilibili-login/ 2019-10-21T04:26:46.838Z 2019-10-21T13:33:57.637Z

登陆时间: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


【1x00】思维导图


01
  • 利用自动化测试工具 Selenium 直接模拟人的行为方式来完成验证

  • 分析页面,想办法找到滑动验证码的完整图片、带有缺口的图片和需要滑动的图片

  • 对比原始的图片和带缺口的图片的像素,像素不同的地方就是缺口位置

  • 计算出滑块缺口的位置,得到所需要滑动的距离

  • 拖拽时要模仿人的行为,由于有个对准过程,所以要构造先快后慢的运动轨迹

  • 最后利用 Selenium 进行对滑块的拖拽


02

【2x00】登陆模块

【2x01】初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
def init():
global url, browser, username, password, wait
url = 'https://passport.bilibili.com/login'
# path是谷歌浏览器驱动的目录,如果已经将目录添加到系统变量,则不用设置此路径
path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe'
chrome_options = Options()
chrome_options.add_argument('--start-maximized')
browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
# 你的哔哩哔哩用户名
username = '155********'
# 你的哔哩哔哩登陆密码
password = '***********'
wait = WebDriverWait(browser, 20)

global 关键字定义了发起请求的url、用户名、密码等全局变量,随后是登录页面url、谷歌浏览器驱动的目录path、实例化 Chrome 浏览器、设置浏览器分辨率最大化、用户名、密码、WebDriverWait() 方法设置等待超时


【2x02】登陆函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def login():
browser.get(url)
# 获取用户名输入框
user = wait.until(EC.presence_of_element_located((By.ID, 'login-username')))
# 获取密码输入框
passwd = wait.until(EC.presence_of_element_located((By.ID, 'login-passwd')))
# 输入用户名
user.send_keys(username)
# 输入密码
passwd.send_keys(password)
# 获取登录按钮
login_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'a.btn.btn-login')))
# 随机暂停几秒
time.sleep(random.random() * 3)
# 点击登陆按钮
login_btn.click()

等待用户名输入框和密码输入框对应的 ID 节点加载出来

获取这两个节点,用户名输入框 id="login-username",密码输入框 id="login-passwd"

调用 send_keys() 方法输入用户名和密码

获取登录按钮 class="btn btn-login"

随机产生一个数并将其扩大三倍作为暂停时间

最后调用 click() 方法实现登录按钮的点击


【3x00】验证码处理模块

【3x01】验证码元素查找函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def find_element():
# 获取带有缺口的图片
c_background = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_bg.geetest_absolute')))
# 获取需要滑动的图片
c_slice = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_slice.geetest_absolute')))
# 获取完整的图片
c_full_bg = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_fullbg.geetest_fade.geetest_absolute')))
# 隐藏需要滑动的图片
hide_element(c_slice)
# 保存带有缺口的图片
save_screenshot(c_background, 'back')
# 显示需要滑动的图片
show_element(c_slice)
# 保存需要滑动的图片
save_screenshot(c_slice, 'slice')
# 显示完整的图片
show_element(c_full_bg)
# 保存完整的图片
save_screenshot(c_full_bg, 'full')

获取验证码的三张图片,分别是完整的图片、带有缺口的图片和需要滑动的图片

分析页面代码,三张图片是由 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() 函数,进一步对验证码进行处理


03

【3x02】元素可见性设置函数

1
2
3
4
5
6
7
8
# 设置元素不可见
def hide_element(element):
browser.execute_script("arguments[0].style=arguments[1]", element, "display: none;")


# 设置元素可见
def show_element(element):
browser.execute_script("arguments[0].style=arguments[1]", element, "display: block;")

【3x03】验证码截图函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def save_screenshot(obj, name):
try:
# 首先对出现验证码后的整个页面进行截图保存
pic_url = browser.save_screenshot('.\\bilibili.png')
print("%s:截图成功!" % pic_url)
# 计算传入的obj,也就是三张图片的位置信息
left = obj.location['x']
top = obj.location['y']
right = left + obj.size['width']
bottom = top + obj.size['height']
# 打印输出一下每一张图的位置信息
print('图:' + name)
print('Left %s' % left)
print('Top %s' % top)
print('Right %s' % right)
print('Bottom %s' % bottom)
print('')
# 在整个页面截图的基础上,根据位置信息,分别剪裁出三张验证码图片并保存
im = Image.open('.\\bilibili.png')
im = im.crop((left, top, right, bottom))
file_name = 'bili_' + name + '.png'
im.save(file_name)
except BaseException as msg:
print("%s:截图失败!" % msg)

location 属性可以返回该图片对象在浏览器中的位置,坐标轴是以屏幕左上角为原点,x轴向右递增,y轴向下递增

size 属性可以返回该图片对象的高度和宽度,由此可以得到验证码的位置信息

首先调用 save_screenshot() 属性对整个页面截图并保存

然后向 crop() 方法传入验证码的位置信息,由位置信息再对验证码进行剪裁并保存


【4x00】验证码滑动模块

【4x01】滑动主函数

1
2
3
4
5
6
def slide():
distance = get_distance(Image.open('.\\bili_back.png'), Image.open('.\\bili_full.png'))
print('计算偏移量为:%s Px' % distance)
trace = get_trace(distance - 5)
move_to_gap(trace)
time.sleep(3)

get_distance() 函数传入完整的图片和缺口图片,计算滑块需要滑动的距离,再把距离信息传入 get_trace() 函数,构造滑块的移动轨迹,最后根据轨迹信息调用 move_to_gap() 函数移动滑块完成验证


【4x02】缺口位置寻找函数

1
2
3
4
5
6
7
8
9
10
11
12
def is_pixel_equal(bg_image, fullbg_image, x, y):
# 获取两张图片对应像素点的RGB数据
bg_pixel = bg_image.load()[x, y]
fullbg_pixel = fullbg_image.load()[x, y]
# 设定一个阈值
threshold = 60
# 比较两张图 RGB 的绝对值是否均小于定义的阈值
if (abs(bg_pixel[0] - fullbg_pixel[0] < threshold) and abs(bg_pixel[1] - fullbg_pixel[1] < threshold) and abs(
bg_pixel[2] - fullbg_pixel[2] < threshold)):
return True
else:
return False

将完整图片和缺口图片两个对象分别赋值给变量 bg_imagefullbg_image,接下来对比图片获取缺口。遍历图片的每个坐标点,获取两张图片对应像素点的 RGB 数据,判断像素的各个颜色之差,abs() 用于取绝对值,比较两张图 RGB 的绝对值是否均小于定义的阈值 threshold,如果绝对值均在阈值之内,则代表像素点相同,继续遍历,否则代表不相同的像素点,即缺口的位置


【4x03】计算滑块移动距离函数

1
2
3
4
5
6
7
8
9
def get_distance(bg_image, fullbg_image):
# 滑块的初始位置
distance = 60
# 遍历两张图片的每个像素
for i in range(distance, fullbg_image.size[0]):
for j in range(fullbg_image.size[1]):
# 调用缺口位置寻找函数
if not is_pixel_equal(fullbg_image, bg_image, i, j):
return i

get_distance() 方法即获取缺口位置的方法,此方法的参数是两张图片,一张为完整的图片,另一张为带缺口的图片,distance 为滑块的初始位置,遍历两张图片的每个像素,利用 is_pixel_equal() 缺口位置寻找函数判断两张图片同一位置的像素是否相同,若不相同则返回该点的值


【4x04】构造移动轨迹函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def get_trace(distance):
trace = []
# 设置加速距离为总距离的4/5
faster_distance = distance * (4 / 5)
# 设置初始位置、初始速度、时间间隔
start, v0, t = 0, 0, 0.1
while start < distance:
if start < faster_distance:
a = 10
else:
a = -10
# 位移
move = v0 * t + 1 / 2 * a * t * t
# 当前时刻的速度
v = v0 + a * t
v0 = v
start += move
trace.append(round(move))
# trace 记录了每个时间间隔移动了多少位移
return trace

get_trace() 方法传入的参数为移动的总距离,返回的是运动轨迹,运动轨迹用 trace 表示,它是一个列表,列表的每个元素代表每次移动多少距离,利用 Selenium 进行对滑块的拖拽时要模仿人的行为,由于有个对准过程,所以是先快后慢,匀速移动、随机速度移动都不会成功,因此要设置一个加速和减速的距离,这里设置加速距离 faster_distance 是总距离 distance 的4/5倍,滑块滑动的加速度用 a 来表示,当前速度用 v 表示,初速度用 v0 表示,位移用 move 表示,所需时间用 t 表示,它们之间满足以下关系:

1
2
move = v0 * t + 0.5 * a * t * t 
v = v0 + a * t

设置初始位置、初始速度、时间间隔分别为0, 0, 0.1,加速阶段和减速阶段的加速度分别设置为10和-10,直到运动轨迹达到总距离时,循环终止,最后得到的 trace 记录了每个时间间隔移动了多少位移,这样滑块的运动轨迹就得到了


【4x05】模拟拖动函数

1
2
3
4
5
6
7
8
9
10
11
12
def move_to_gap(trace):
# 获取滑动按钮
slider = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div.geetest_slider_button')))
# 点击并拖动滑块
ActionChains(browser).click_and_hold(slider).perform()
# 遍历运动轨迹获取每小段位移距离
for x in trace:
# 移动此位移
ActionChains(browser).move_by_offset(xoffset=x, yoffset=0).perform()
time.sleep(0.5)
# 释放鼠标
ActionChains(browser).release().perform()

传入的参数为运动轨迹,首先查找到滑动按钮,然后调用 ActionChains 的 click_and_hold() 方法按住拖动底部滑块,perform() 方法用于执行,遍历运动轨迹获取每小段位移距离,调用 move_by_offset() 方法移动此位移,最后调用 release() 方法松开鼠标即可


【5x00】完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# =============================================
# --*-- coding: utf-8 --*--
# @Time : 2019-10-21
# @Author : TRHX
# @Blog : www.itrhx.com
# @CSDN : https://blog.csdn.net/qq_36759224
# @FileName: bilibili.py
# @Software: PyCharm
# =============================================

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
import time
import random
from PIL import Image


# 初始化函数
def init():
global url, browser, username, password, wait
url = 'https://passport.bilibili.com/login'
# path是谷歌浏览器驱动的目录,如果已经将目录添加到系统变量,则不用设置此路径
path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe'
chrome_options = Options()
chrome_options.add_argument('--start-maximized')
browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
# 你的哔哩哔哩用户名
username = '155********'
# 你的哔哩哔哩登录密码
password = '***********'
wait = WebDriverWait(browser, 20)


# 登录函数
def login():
browser.get(url)
# 获取用户名输入框
user = wait.until(EC.presence_of_element_located((By.ID, 'login-username')))
# 获取密码输入框
passwd = wait.until(EC.presence_of_element_located((By.ID, 'login-passwd')))
# 输入用户名
user.send_keys(username)
# 输入密码
passwd.send_keys(password)
# 获取登录按钮
login_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'a.btn.btn-login')))
# 随机暂停几秒
time.sleep(random.random() * 3)
# 点击登陆按钮
login_btn.click()


# 验证码元素查找函数
def find_element():
# 获取带有缺口的图片
c_background = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_bg.geetest_absolute')))
# 获取需要滑动的图片
c_slice = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_slice.geetest_absolute')))
# 获取完整的图片
c_full_bg = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_fullbg.geetest_fade.geetest_absolute')))
# 隐藏需要滑动的图片
hide_element(c_slice)
# 保存带有缺口的图片
save_screenshot(c_background, 'back')
# 显示需要滑动的图片
show_element(c_slice)
# 保存需要滑动的图片
save_screenshot(c_slice, 'slice')
# 显示完整的图片
show_element(c_full_bg)
# 保存完整的图片
save_screenshot(c_full_bg, 'full')


# 设置元素不可见
def hide_element(element):
browser.execute_script("arguments[0].style=arguments[1]", element, "display: none;")


# 设置元素可见
def show_element(element):
browser.execute_script("arguments[0].style=arguments[1]", element, "display: block;")


# 验证码截图函数
def save_screenshot(obj, name):
try:
# 首先对出现验证码后的整个页面进行截图保存
pic_url = browser.save_screenshot('.\\bilibili.png')
print("%s:截图成功!" % pic_url)
# 计算传入的obj,也就是三张图片的位置信息
left = obj.location['x']
top = obj.location['y']
right = left + obj.size['width']
bottom = top + obj.size['height']
# 打印输出一下每一张图的位置信息
print('图:' + name)
print('Left %s' % left)
print('Top %s' % top)
print('Right %s' % right)
print('Bottom %s' % bottom)
print('')
# 在整个页面截图的基础上,根据位置信息,分别剪裁出三张验证码图片并保存
im = Image.open('.\\bilibili.png')
im = im.crop((left, top, right, bottom))
file_name = 'bili_' + name + '.png'
im.save(file_name)
except BaseException as msg:
print("%s:截图失败!" % msg)


# 滑动模块的主函数
def slide():
distance = get_distance(Image.open('.\\bili_back.png'), Image.open('.\\bili_full.png'))
print('计算偏移量为:%s Px' % distance)
trace = get_trace(distance - 5)
move_to_gap(trace)
time.sleep(3)


# 计算滑块移动距离函数
def get_distance(bg_image, fullbg_image):
# 滑块的初始位置
distance = 60
# 遍历两张图片的每个像素
for i in range(distance, fullbg_image.size[0]):
for j in range(fullbg_image.size[1]):
# 调用缺口位置寻找函数
if not is_pixel_equal(fullbg_image, bg_image, i, j):
return i


# 缺口位置寻找函数
def is_pixel_equal(bg_image, fullbg_image, x, y):
# 获取两张图片对应像素点的RGB数据
bg_pixel = bg_image.load()[x, y]
fullbg_pixel = fullbg_image.load()[x, y]
# 设定一个阈值
threshold = 60
# 比较两张图 RGB 的绝对值是否均小于定义的阈值
if (abs(bg_pixel[0] - fullbg_pixel[0] < threshold) and abs(bg_pixel[1] - fullbg_pixel[1] < threshold) and abs(
bg_pixel[2] - fullbg_pixel[2] < threshold)):
return True
else:
return False


# 构造移动轨迹函数
def get_trace(distance):
trace = []
# 设置加速距离为总距离的4/5
faster_distance = distance * (4 / 5)
# 设置初始位置、初始速度、时间间隔
start, v0, t = 0, 0, 0.1
while start < distance:
if start < faster_distance:
a = 10
else:
a = -10
# 位移
move = v0 * t + 1 / 2 * a * t * t
# 当前时刻的速度
v = v0 + a * t
v0 = v
start += move
trace.append(round(move))
# trace 记录了每个时间间隔移动了多少位移
return trace


# 模拟拖动函数
def move_to_gap(trace):
# 获取滑动按钮
slider = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div.geetest_slider_button')))
# 点击并拖动滑块
ActionChains(browser).click_and_hold(slider).perform()
# 遍历运动轨迹获取每小段位移距离
for x in trace:
# 移动此位移
ActionChains(browser).move_by_offset(xoffset=x, yoffset=0).perform()
time.sleep(0.5)
# 释放鼠标
ActionChains(browser).release().perform()


if __name__ == '__main__':
init()
login()
find_element()
slide()

【6x00】效果实现动图

最终实现效果图:(关键信息已经过打码处理)


04
]]>
<blockquote> <p>登陆时间:2019-10-21<br>实现难度:★★★☆☆☆<br>请求链接:<a href="https://passport.bilibili.com/login" target="_blank" rel="noopener">https://passport.bilibili.com/login</a><br>实现目标:模拟登陆哔哩哔哩,攻克滑动验证码<br>涉及知识:滑动验证码的攻克、自动化测试工具 Selenium 的使用<br>完整代码:<a href="https://github.com/TRHX/Python3-Spider-Practice/tree/master/bilibili-login" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice/tree/master/bilibili-login</a><br>其他爬虫实战代码合集(持续更新):<a href="https://github.com/TRHX/Python3-Spider-Practice" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice</a><br>爬虫实战专栏(持续更新):<a href="https://itrhx.blog.csdn.net/article/category/9351278" target="_blank" rel="noopener">https://itrhx.blog.csdn.net/article/category/9351278</a></p> </blockquote> <hr>
Python3 爬虫实战 — 虎扑论坛步行街 https://www.itrhx.com/2019/10/12/A55-pyspider-hupu/ 2019-10-12T15:28:23.380Z 2019-10-21T04:08:46.598Z

爬取时间: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


【1x00】循环爬取网页模块

观察虎扑论坛步行街分区,请求地址为:https://bbs.hupu.com/bxj

第一页: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
2
3
4
5
6
7
8
9
10
11
12
def get_pages(page_url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}
response = requests.get(url=page_url, headers=headers)
page_soup = BeautifulSoup(response.text, 'lxml')
return page_soup

if __name__ == '__main__':
for i in range(1, 11):
url = 'https://bbs.hupu.com/bxj-' + str(i)
soup = get_pages(url)

【2x00】解析模块

使用 Beautiful Soup 对网页各个信息进行提取,最后将这些信息放进一个列表里,然后调用列表的 .append() 方法,再将每条帖子的列表依次加到另一个新列表里,最终返回的是类似于如下形式的列表:

1
[['帖子1', '作者1'], ['帖子2', '作者2'], ['帖子3', '作者3']]

这样做的目的是:方便 MongoDB 依次储存每一条帖子的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def parse_pages(page_soup):
data_list = []
all_list = page_soup.find('ul', class_='for-list')
post_list = all_list.find_all('li')
# print(result_list)
for post in post_list:
# 帖子名称
post_title = post.find('a', class_='truetit').text
# print(post_title)
# 帖子链接
post_url = 'https://bbs.hupu.com' + post.find('a', class_='truetit')['href']
# print(post_url)
# 作者
author = post.select('.author > a')[0].text
# print(author)
# 作者主页
author_url = post.select('.author > a')[0]['href']
# print(author_url)
# 发布日期
post_date = post.select('.author > a')[1].text
# print(post_date)
reply_view = post.find('span', class_='ansour').text
# 回复数
post_reply = reply_view.split('/')[0].strip()
# print(post_reply)
# 浏览量
post_view = reply_view.split('/')[1].strip()
# print(post_view)
# 最后回复时间
last_data = post.select('.endreply > a')[0].text
# print(last_data)
# 最后回复用户
last_user = post.select('.endreply > span')[0].text
# print(last_user)

data_list.append([post_title, post_url, author, author_url, post_date, post_reply, post_view, last_data, last_user])

# print(data_list)
return data_list

【3x00】MongoDB 数据储存模块

首先使用 MongoClient() 方法,向其传入地址参数 host 和 端口参数 port,指定数据库为 hupu,集合为 bxj

将解析函数返回的列表传入到储存函数,依次循环该列表,对每一条帖子的信息进行提取并储存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def mongodb(data_list):
client = MongoClient('localhost', 27017)
db = client.hupu
collection = db.bxj
for data in data_list:
bxj = {
'帖子名称': data[0],
'帖子链接': data[1],
'作者': data[2],
'作者主页': data[3],
'发布日期': str(data[4]),
'回复数': data[5],
'浏览量': data[6],
'最后回复时间': str(data[7]),
'最后回复用户': data[8]
}
collection.insert_one(bxj)

【4x00】完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# =============================================
# --*-- coding: utf-8 --*--
# @Time : 2019-10-12
# @Author : TRHX
# @Blog : www.itrhx.com
# @CSDN : https://blog.csdn.net/qq_36759224
# @FileName: hupu.py
# @Software: PyCharm
# =============================================

import requests
import time
import random
from pymongo import MongoClient
from bs4 import BeautifulSoup


def get_pages(page_url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}
response = requests.get(url=page_url, headers=headers)
page_soup = BeautifulSoup(response.text, 'lxml')
return page_soup


def parse_pages(page_soup):
data_list = []
all_list = page_soup.find('ul', class_='for-list')
post_list = all_list.find_all('li')
# print(result_list)
for post in post_list:
# 帖子名称
post_title = post.find('a', class_='truetit').text
# print(post_title)
# 帖子链接
post_url = 'https://bbs.hupu.com' + post.find('a', class_='truetit')['href']
# print(post_url)
# 作者
author = post.select('.author > a')[0].text
# print(author)
# 作者主页
author_url = post.select('.author > a')[0]['href']
# print(author_url)
# 发布日期
post_date = post.select('.author > a')[1].text
# print(post_date)
reply_view = post.find('span', class_='ansour').text
# 回复数
post_reply = reply_view.split('/')[0].strip()
# print(post_reply)
# 浏览量
post_view = reply_view.split('/')[1].strip()
# print(post_view)
# 最后回复时间
last_data = post.select('.endreply > a')[0].text
# print(last_data)
# 最后回复用户
last_user = post.select('.endreply > span')[0].text
# print(last_user)

data_list.append([post_title, post_url, author, author_url, post_date, post_reply, post_view, last_data, last_user])

# print(data_list)
return data_list


def mongodb(data_list):
client = MongoClient('localhost', 27017)
db = client.hupu
collection = db.bxj
for data in data_list:
bxj = {
'帖子名称': data[0],
'帖子链接': data[1],
'作者': data[2],
'作者主页': data[3],
'发布日期': str(data[4]),
'回复数': data[5],
'浏览量': data[6],
'最后回复时间': str(data[7]),
'最后回复用户': data[8]
}
collection.insert_one(bxj)


if __name__ == '__main__':
for i in range(1, 11):
url = 'https://bbs.hupu.com/bxj-' + str(i)
soup = get_pages(url)
result_list = parse_pages(soup)
mongodb(result_list)
print('第', i, '页数据爬取完毕!')
time.sleep(random.randint(3, 10))
print('前10页所有数据爬取完毕!')

【5x00】数据截图

一共爬取到 1180 条数据:


01

【6x00】程序不足的地方

程序只能爬取前 10 页的数据,因为虎扑论坛要求从第 11 页开始,必须登录账号才能查看,并且登录时会有智能验证,可以使用自动化测试工具 Selenium 模拟登录账号后再进行爬取。

]]>
<blockquote> <p>爬取时间:2019-10-12<br>爬取难度:★★☆☆☆☆<br>请求链接:<a href="https://bbs.hupu.com/bxj" target="_blank" rel="noopener">https://bbs.hupu.com/bxj</a><br>爬取目标:爬取虎扑论坛步行街的帖子,包含主题,作者,发布时间等,数据保存到 MongoDB 数据库<br>涉及知识:请求库 requests、解析库 Beautiful Soup、数据库 MongoDB 的操作<br>完整代码:<a href="https://github.com/TRHX/Python3-Spider-Practice/tree/master/hupu" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice/tree/master/hupu</a><br>其他爬虫实战代码合集(持续更新):<a href="https://github.com/TRHX/Python3-Spider-Practice" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice</a><br>爬虫实战专栏(持续更新):<a href="https://itrhx.blog.csdn.net/article/category/9351278" target="_blank" rel="noopener">https://itrhx.blog.csdn.net/article/category/9351278</a></p> </blockquote> <hr>
Python3 爬虫实战 — 安居客武汉二手房 https://www.itrhx.com/2019/10/09/A54-pyspider-anjuke/ 2019-10-09T15:02:42.994Z 2019-10-21T04:05:48.158Z

爬取时间: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


【1x00】页面整体分析

分析 安居客武汉二手房页面,这次爬取实战准备使用 BeautifulSoup 解析库,熟练 BeautifulSoup 解析库的用法,注意到该页面与其他页面不同的是,不能一次性看到到底有多少页,以前知道一共有多少页,直接一个循环爬取就行了,虽然可以通过改变 url 来尝试找到最后一页,但是这样就显得不程序员了😂,因此可以通过 BeautifulSoup 解析 下一页按钮,提取到下一页的 url,直到没有 下一页按钮 这个元素为止,从而实现所有页面的爬取,剩下的信息提取和储存就比较简单了


【2x00】解析模块

分析页面,可以发现每条二手房信息都是包含在 <li> 标签内的,因此可以使用 BeautifulSoup 解析页面得到所有的 <li> 标签,然后再循环访问每个 <li> 标签,依次解析得到每条二手房的各种信息


01
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def parse_pages(url, num):
response = requests.get(url=url, headers=headers)
soup = BeautifulSoup(response.text, 'lxml')
result_list = soup.find_all('li', class_='list-item')
# print(len(result_list))
for result in result_list:
# 标题
title = result.find('a', class_='houseListTitle').text.strip()
# print(title)
# 户型
layout = result.select('.details-item > span')[0].text
# print(layout)
# 面积
cover = result.select('.details-item > span')[1].text
# print(cover)
# 楼层
floor = result.select('.details-item > span')[2].text
# print(floor)
# 建造年份
year = result.select('.details-item > span')[3].text
# print(year)
# 单价
unit_price = result.find('span', class_='unit-price').text.strip()
# print(unit_price)
# 总价
total_price = result.find('span', class_='price-det').text.strip()
# print(total_price)
# 关键字
keyword = result.find('div', class_='tags-bottom').text.strip()
# print(keyword)
# 地址
address = result.find('span', class_='comm-address').text.replace(' ', '').replace('\n', '')
# print(address)
# 详情页url
details_url = result.find('a', class_='houseListTitle')['href']
# print(details_url)

if __name__ == '__main__':
start_num = 0
start_url = 'https://wuhan.anjuke.com/sale/'
parse_pages(start_url, start_num)

【3x00】循环爬取模块

前面已经分析过,该网页是无法一下就能看到一共有多少页的,尝试找到最后一页,发现一共有50页,那么此时就可以搞个循环,一直到第50页就行了,但是如果有一天页面数增加了呢,那么代码的可维护性就不好了,我们可以观察 下一页按钮 ,当存在下一页的时候,是 <a> 标签,并且带有下一页的 URL,不存在下一页的时候是 <i> 标签,因此可以写个 if 语句,判断是否存在此 <a> 标签,若存在,表示有下一页,然后提取其 href 属性并传给解析模块,实现后面所有页面的信息提取,此外,由于安居客有反爬系统,我们还可以利用 Python中的 random.randint() 方法,在两个数值之间随机取一个数,传入 time.sleep() 方法,实现随机暂停爬取


02
1
2
3
4
5
6
7
8
9
10
# 判断是否还有下一页
next_url = soup.find_all('a', class_='aNxt')
if len(next_url) != 0:
num += 1
print('第' + str(num) + '页数据爬取完毕!')
# 3-60秒之间随机暂停
time.sleep(random.randint(3, 60))
parse_pages(next_url[0].attrs['href'], num)
else:
print('所有数据爬取完毕!')

【4x00】数据储存模块

数据储存比较简单,将每个二手房信息组成一个列表,依次写入到 anjuke.csv 文件中即可

1
2
3
4
results = [title, layout, cover, floor, year, unit_price, total_price, keyword, address, details_url]
with open('anjuke.csv', 'a', newline='', encoding='utf-8-sig') as f:
w = csv.writer(f)
w.writerow(results)

【5x00】完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# =============================================
# --*-- coding: utf-8 --*--
# @Time : 2019-10-09
# @Author : TRHX
# @Blog : www.itrhx.com
# @CSDN : https://blog.csdn.net/qq_36759224
# @FileName: anjuke.py
# @Software: PyCharm
# =============================================

import requests
import time
import csv
import random
from bs4 import BeautifulSoup

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}


def parse_pages(url, num):
response = requests.get(url=url, headers=headers)
soup = BeautifulSoup(response.text, 'lxml')
result_list = soup.find_all('li', class_='list-item')
# print(len(result_list))
for result in result_list:
# 标题
title = result.find('a', class_='houseListTitle').text.strip()
# print(title)
# 户型
layout = result.select('.details-item > span')[0].text
# print(layout)
# 面积
cover = result.select('.details-item > span')[1].text
# print(cover)
# 楼层
floor = result.select('.details-item > span')[2].text
# print(floor)
# 建造年份
year = result.select('.details-item > span')[3].text
# print(year)
# 单价
unit_price = result.find('span', class_='unit-price').text.strip()
# print(unit_price)
# 总价
total_price = result.find('span', class_='price-det').text.strip()
# print(total_price)
# 关键字
keyword = result.find('div', class_='tags-bottom').text.strip()
# print(keyword)
# 地址
address = result.find('span', class_='comm-address').text.replace(' ', '').replace('\n', '')
# print(address)
# 详情页url
details_url = result.find('a', class_='houseListTitle')['href']
# print(details_url)
results = [title, layout, cover, floor, year, unit_price, total_price, keyword, address, details_url]
with open('anjuke.csv', 'a', newline='', encoding='utf-8-sig') as f:
w = csv.writer(f)
w.writerow(results)

# 判断是否还有下一页
next_url = soup.find_all('a', class_='aNxt')
if len(next_url) != 0:
num += 1
print('第' + str(num) + '页数据爬取完毕!')
# 3-60秒之间随机暂停
time.sleep(random.randint(3, 60))
parse_pages(next_url[0].attrs['href'], num)
else:
print('所有数据爬取完毕!')


if __name__ == '__main__':
with open('anjuke.csv', 'a', newline='', encoding='utf-8-sig') as fp:
writer = csv.writer(fp)
writer.writerow(['标题', '户型', '面积', '楼层', '建造年份', '单价', '总价', '关键字', '地址', '详情页地址'])
start_num = 0
start_url = 'https://wuhan.anjuke.com/sale/'
parse_pages(start_url, start_num)

【6x00】数据截图


03

【7x00】程序不足的地方

  • 虽然使用了随机暂停爬取的方法,但是在爬取了大约 20 页的数据后依然会出现验证页面,导致程序终止

  • 原来设想的是可以由用户手动输入城市的拼音来查询不同城市的信息,方法是把用户输入的城市拼音和其他参数一起构造成一个 URL,然后对该 URL 发送请求,判断请求返回的代码,如果是 200 就代表可以访问,也就是用户输入的城市是正确的,然而发现即便是输入错误,该 URL 依然可以访问,只不过会跳转到一个正确的页面,没有搞清楚是什么原理,也就无法实现由用户输入城市来查询这个功能

]]>
<blockquote> <p>爬取时间:2019-10-09<br>爬取难度:★★☆☆☆☆<br>请求链接:<a href="https://wuhan.anjuke.com/sale/" target="_blank" rel="noopener">https://wuhan.anjuke.com/sale/</a><br>爬取目标:爬取武汉二手房每一条售房信息,包含地理位置、价格、面积等,保存为 CSV 文件<br>涉及知识:请求库 requests、解析库 Beautiful Soup、CSV 文件储存、列表操作、分页判断<br>完整代码:<a href="https://github.com/TRHX/Python3-Spider-Practice/tree/master/anjuke" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice/tree/master/anjuke</a><br>其他爬虫实战代码合集(持续更新):<a href="https://github.com/TRHX/Python3-Spider-Practice" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice</a><br>爬虫实战专栏(持续更新):<a href="https://itrhx.blog.csdn.net/article/category/9351278" target="_blank" rel="noopener">https://itrhx.blog.csdn.net/article/category/9351278</a></p> </blockquote> <hr>
使用 Hexo-Git-Backup 插件备份你的 Hexo 博客 https://www.itrhx.com/2019/09/29/A53-hexo-backup/ 2019-09-29T10:02:15.603Z 2019-12-29T07:19:49.434Z

欢迎关注我的 CSDN 专栏:《个人博客搭建:Hexo+Github Pages》,从搭建到美化一条龙,帮你解决 Hexo 常见问题!


由于 Hexo 博客是静态托管的,所有的原始数据都保存在本地,如果哪一天电脑坏了,或者是误删了本地数据,那就是叫天天不应叫地地不灵了,此时定时备份就显得比较重要了,常见的备份方法有:打包数据保存到U盘、云盘或者其他地方,但是早就有大神开发了备份插件:hexo-git-backup ,只需要一个命令就可以将所有数据包括主题文件备份到 github 了

首先进入你博客目录,输入命令 hexo version 查看 Hexo 版本,如图所示,我的版本是 3.7.1:


01

安装备份插件,如果你的 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
2
3
4
5
6
7
backup:
type: git
theme: material-x-1.2.1
message: Back up my www.itrhx.com blog
repository:
github: git@github.com:TRHX/TRHX.github.io.git,backup
coding: git@git.dev.tencent.com:TRHX/TRHX.git,backup

参数解释:

  • theme:你要备份的主题名称
  • message:自定义提交信息
  • repository:仓库名,注意仓库地址后面要添加一个分支名,比如我就创建了一个 backup 分支

最后使用以下命令备份你的博客:

1
$ hexo backup

或者使用以下简写命令也可以:

1
$ hexo b

备份成功后可以在你的仓库分支下看到备份的原始文件:


02

03
]]>
一键备份博客数据,再也不怕数据丢失了!
Python3 爬虫实战 — 豆瓣电影TOP250 https://www.itrhx.com/2019/09/28/A52-pyspider-doubantop250/ 2019-09-28T08:35:19.823Z 2019-10-21T04:01:29.248Z

爬取时间: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


【1x00】循环爬取网页模块

观察豆瓣电影 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_urlsm_urls 是一个列表,列表元素就是电影详情页的 URL

1
2
3
4
5
6
7
8
9
10
def index_pages(number):
url = 'https://movie.douban.com/top250?start=%s&filter=' % number
index_response = requests.get(url=url, headers=headers)
tree = etree.HTML(index_response.text)
m_urls = tree.xpath("//li/div/div/a/@href")
return m_urls

if __name__ == '__main__':
for i in range(0, 250, 25):
movie_urls = index_pages(i)

【2x00】解析模块

定义一个解析函数 parse_pages(),利用 for 循环,依次提取 index_pages() 函数返回的列表中的元素,也就是每部电影详情页的 URL,将其传给解析函数进行解析

1
2
3
4
5
6
7
8
9
10
11
def index_pages(number):
expressions

def parse_pages(url):
expressions

if __name__ == '__main__':
for i in range(0, 250, 25):
movie_urls = index_pages(i)
for movie_url in movie_urls:
results = parse_pages(movie_url)

详细看一下解析函数 parse_pages(),首先要对接收到的详情页 URL 发送请求,获取响应内容,然后再使用 Xpath 提取相关信息

1
2
3
def parse_pages(url):
movie_pages = requests.get(url=url, headers=headers)
parse_movie = etree.HTML(movie_pages.text)

【2x01】Xpath 解析排名、电影名、评分信息

其中排名、电影名和评分信息是最容易匹配到的,直接使用 Xpath 语法就可以轻松解决:

1
2
3
4
5
6
7
8
# 排名
ranking = parse_movie.xpath("//span[@class='top250-no']/text()")

# 电影名
name = parse_movie.xpath("//h1/span[1]/text()")

# 评分
score = parse_movie.xpath("//div[@class='rating_self clearfix']/strong/text()")

【2x02】Xpath 解析参评人数

接下来准备爬取有多少人参与了评价,分析一下页面:


01

如果只爬取这个 <span> 标签下的数字的话,没有任何提示信息,别人看了不知道是啥东西,所以把 人评价 这三个字也爬下来的话就比较好了,但是可以看到数字和文字不在同一个元素标签下,而且文字部分还有空格,要爬取的话就要把 class="rating_people"a 标签下所有的 text 提取出来,然后再去掉空格:

1
2
3
4
5
6
7
8
9
# 参评人数
# 匹配a节点
value = parse_movie.xpath("//a[@class='rating_people']")
# 提取a节点下所有文本
string = [value[0].xpath('string(.)')]
# 去除多余空格
number = [a.strip() for a in string]

# 此时 number = ['1617307人评价']

这样做太麻烦了,我们可以直接提取数字,得到一个列表,然后使用另一个带有提示信息的列表,将两个列表的元素合并,组成一个新列表,这个新列表的元素就是提示信息+人数

1
2
3
4
5
6
# 参评人数
value = parse_movie.xpath("//span[@property='v:votes']/text()")
# 合并元素
number = [" ".join(['参评人数:'] + value)]

# 此时 number = ['参评人数:1617307']


【2x03】正则表达式解析制片国家、语言

接下来尝试爬取制片国家/地区、语言等信息:


02

分析页面可以观察到,制片国家/地区和语言结构比较特殊,没有特别的 class 或者 id 属性,所包含的层次关系也太复杂,所以这里为了简便,直接采用正则表达式来匹配信息,就没有那么复杂了:

1
2
3
4
5
6
7
# 制片国家/地区
value = re.findall('<span class="pl">制片国家/地区:</span>(.*?)<br/>', movie_pages.text)
country = [" ".join(['制片国家:'] + value)]

# 语言
value = re.findall('<span class="pl">语言:</span>(.*?)<br/>', movie_pages.text)
language = [" ".join(['语言:'] + value)]

【3x00】返回解析数据

其他剩下的信息皆可利用以上方法进行提取,所有信息提取完毕,最后使用 zip() 函数,将所有提取的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表

1
return zip(ranking, name, score, number, types, country, language, date, time, other_name, director, screenwriter, performer, m_url, imdb_url)

【4x00】数据储存模块

定义一个数据保存函数 save_results()

1
2
3
4
def save_results(data):
with open('douban.csv', 'a', encoding="utf-8-sig") as fp:
writer = csv.writer(fp)
writer.writerow(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
2
3
4
5
6
7
8
9
10
11
# 保存电影海报
poster = parse_movie.xpath("//div[@id='mainpic']/a/img/@src")
response = requests.get(poster[0])
name2 = re.sub(r'[A-Za-z\:\s]', '', name[0])
poster_name = str(ranking[0]) + ' - ' + name2 + '.jpg'
dir_name = 'douban_poster'
if not os.path.exists(dir_name):
os.mkdir(dir_name)
poster_path = dir_name + '/' + poster_name
with open(poster_path, "wb")as f:
f.write(response.content)

解析电影详情页,使用 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 文件夹下


【5x00】完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# =============================================
# --*-- coding: utf-8 --*--
# @Time : 2019-09-27
# @Author : TRHX
# @Blog : www.itrhx.com
# @CSDN : https://blog.csdn.net/qq_36759224
# @FileName: douban.py
# @Software: PyCharm
# =============================================

import requests
from lxml import etree
import csv
import re
import time
import os

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'}


def index_pages(number):
url = 'https://movie.douban.com/top250?start=%s&filter=' % number
index_response = requests.get(url=url, headers=headers)
tree = etree.HTML(index_response.text)
m_urls = tree.xpath("//li/div/div/a/@href")
return m_urls


def parse_pages(url):
movie_pages = requests.get(url=url, headers=headers)
parse_movie = etree.HTML(movie_pages.text)

# 排名
ranking = parse_movie.xpath("//span[@class='top250-no']/text()")

# 电影名
name = parse_movie.xpath("//h1/span[1]/text()")

# 评分
score = parse_movie.xpath("//div[@class='rating_self clearfix']/strong/text()")

# 参评人数
value = parse_movie.xpath("//span[@property='v:votes']/text()")
number = [" ".join(['参评人数:'] + value)]
# value = parse_movie.xpath("//a[@class='rating_people']")
# string = [value[0].xpath('string(.)')]
# number = [a.strip() for a in string]
# print(number)

# 类型
value = parse_movie.xpath("//span[@property='v:genre']/text()")
types = [" ".join(['类型:'] + value)]

# 制片国家/地区
value = re.findall('<span class="pl">制片国家/地区:</span>(.*?)<br/>', movie_pages.text)
country = [" ".join(['制片国家:'] + value)]

# 语言
value = re.findall('<span class="pl">语言:</span>(.*?)<br/>', movie_pages.text)
language = [" ".join(['语言:'] + value)]

# 上映时期
value = parse_movie.xpath("//span[@property='v:initialReleaseDate']/text()")
date = [" ".join(['上映日期:'] + value)]

# 片长
value = parse_movie.xpath("//span[@property='v:runtime']/text()")
time = [" ".join(['片长:'] + value)]

# 又名
value = re.findall('<span class="pl">又名:</span>(.*?)<br/>', movie_pages.text)
other_name = [" ".join(['又名:'] + value)]

# 导演
value = parse_movie.xpath("//div[@id='info']/span[1]/span[@class='attrs']/a/text()")
director = [" ".join(['导演:'] + value)]

# 编剧
value = parse_movie.xpath("//div[@id='info']/span[2]/span[@class='attrs']/a/text()")
screenwriter = [" ".join(['编剧:'] + value)]

# 主演
value = parse_movie.xpath("//div[@id='info']/span[3]")
performer = [value[0].xpath('string(.)')]

# URL
m_url = ['豆瓣链接:' + movie_url]

# IMDb链接
value = parse_movie.xpath("//div[@id='info']/a/@href")
imdb_url = [" ".join(['IMDb链接:'] + value)]

# 保存电影海报
poster = parse_movie.xpath("//div[@id='mainpic']/a/img/@src")
response = requests.get(poster[0])
name2 = re.sub(r'[A-Za-z\:\s]', '', name[0])
poster_name = str(ranking[0]) + ' - ' + name2 + '.jpg'
dir_name = 'douban_poster'
if not os.path.exists(dir_name):
os.mkdir(dir_name)
poster_path = dir_name + '/' + poster_name
with open(poster_path, "wb")as f:
f.write(response.content)

return zip(ranking, name, score, number, types, country, language, date, time, other_name, director, screenwriter, performer, m_url, imdb_url)


def save_results(data):
with open('douban.csv', 'a', encoding="utf-8-sig") as fp:
writer = csv.writer(fp)
writer.writerow(data)


if __name__ == '__main__':
num = 0
for i in range(0, 250, 25):
movie_urls = index_pages(i)
for movie_url in movie_urls:
results = parse_pages(movie_url)
for result in results:
num += 1
save_results(result)
print('第' + str(num) + '条电影信息保存完毕!')
time.sleep(3)

【6x00】数据截图


03

04

【7x00】程序不足的地方

程序不足的地方:豆瓣电影有反爬机制,当程序爬取到大约 150 条数据的时候,IP 就会被封掉,第二天 IP 才会解封,可以考虑综合使用多个代理、多个 User-Agent、随机时间暂停等方法进行爬取

]]>
<blockquote> <p>爬取时间:2019-09-27<br>爬取难度:★★☆☆☆☆<br>请求链接:<a href="https://movie.douban.com/top250" target="_blank" rel="noopener">https://movie.douban.com/top250</a> 以及每部电影详情页<br>爬取目标:爬取榜单上每一部电影详情页的数据,保存为 CSV 文件;下载所有电影海报到本地<br>涉及知识:请求库 requests、解析库 lxml、Xpath 语法、正则表达式、CSV 和二进制数据储存、列表操作<br>完整代码:<a href="https://github.com/TRHX/Python3-Spider-Practice/tree/master/douban-top250" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice/tree/master/douban-top250</a><br>其他爬虫实战代码合集(持续更新):<a href="https://github.com/TRHX/Python3-Spider-Practice" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice</a><br>爬虫实战专栏(持续更新):<a href="https://itrhx.blog.csdn.net/article/category/9351278" target="_blank" rel="noopener">https://itrhx.blog.csdn.net/article/category/9351278</a></p> </blockquote> <hr>
Python3 爬虫实战 — 猫眼电影TOP100 https://www.itrhx.com/2019/09/24/A51-pyspider-maoyantop100/ 2019-09-24T11:31:56.965Z 2019-10-21T04:00:20.669Z

爬取时间: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


【1x00】循环爬取网页模块

观察猫眼电影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
2
3
4
5
6
7
8
def index_page(number):
url = 'https://maoyan.com/board/4?offset=%s' % number
response = requests.get(url=url, headers=headers)
return response.text

if __name__ == '__main__':
for i in range(0, 100, 10):
index = index_page(i)

【2x00】解析模块

定义一个页面解析函数 parse_page(),使用 lxml 解析库的 Xpath 方法依次提取电影排名(ranking)、电影名称(movie_name)、主演(performer)、上映时间(releasetime)、评分(score)、电影封面图 url(movie_img)

通过对主演部分的提取发现有多余的空格符和换行符,循环 performer 列表,使用 strip() 方法去除字符串头尾空格和换行符

电影评分分为整数部分和小数部分,依次提取两部分,循环遍历组成一个完整的评分

最后使用 zip() 函数,将所有提取的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def parse_page(content):
tree = etree.HTML(content)
# 电影排名
ranking = tree.xpath("//dd/i/text()")
# 电影名称
movie_name = tree.xpath('//p[@class="name"]/a/text()')
# 主演
performer = tree.xpath("//p[@class='star']/text()")
performer = [p.strip() for p in performer]
# 上映时间
releasetime = tree.xpath('//p[@class="releasetime"]/text()')
# 评分
score1 = tree.xpath('//p[@class="score"]/i[@class="integer"]/text()')
score2 = tree.xpath('//p[@class="score"]/i[@class="fraction"]/text()')
score = [score1[i] + score2[i] for i in range(min(len(score1), len(score2)))]
# 电影封面图
movie_img = tree.xpath('//img[@class="board-img"]/@data-src')
return zip(ranking, movie_name, performer, releasetime, score, movie_img)

【3x00】数据储存模块

定义一个 save_results() 函数,将所有数据保存到 maoyan.csv 文件

1
2
3
4
def save_results(result):
with open('maoyan.csv', 'a') as fp:
writer = csv.writer(fp)
writer.writerow(result)

【4x00】完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# =============================================
# --*-- coding: utf-8 --*--
# @Time : 2019-09-23
# @Author : TRHX
# @Blog : www.itrhx.com
# @CSDN : https://blog.csdn.net/qq_36759224
# @FileName: maoyan.py
# @Software: PyCharm
# =============================================

import requests
from lxml import etree
import csv

headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'
}


def index_page(number):
url = 'https://maoyan.com/board/4?offset=%s' % number
response = requests.get(url=url, headers=headers)
return response.text


def parse_page(content):
tree = etree.HTML(content)
# 电影排名
ranking = tree.xpath("//dd/i/text()")
# 电影名称
movie_name = tree.xpath('//p[@class="name"]/a/text()')
# 主演
performer = tree.xpath("//p[@class='star']/text()")
performer = [p.strip() for p in performer]
# 上映时间
releasetime = tree.xpath('//p[@class="releasetime"]/text()')
# 评分
score1 = tree.xpath('//p[@class="score"]/i[@class="integer"]/text()')
score2 = tree.xpath('//p[@class="score"]/i[@class="fraction"]/text()')
score = [score1[i] + score2[i] for i in range(min(len(score1), len(score2)))]
# 电影封面图
movie_img = tree.xpath('//img[@class="board-img"]/@data-src')
return zip(ranking, movie_name, performer, releasetime, score, movie_img)


def save_results(result):
with open('maoyan.csv', 'a') as fp:
writer = csv.writer(fp)
writer.writerow(result)


if __name__ == '__main__':
print('开始爬取数据...')
for i in range(0, 100, 10):
index = index_page(i)
results = parse_page(index)
for i in results:
save_results(i)
print('数据爬取完毕!')

【4x00】数据截图


01
]]>
<blockquote> <p>爬取时间:2019-09-23<br>爬取难度:★☆☆☆☆☆<br>请求链接:<a href="https://maoyan.com/board/4" target="_blank" rel="noopener">https://maoyan.com/board/4</a><br>爬取目标:猫眼 TOP100 的电影名称、排名、主演、上映时间、评分、封面图地址,数据保存为 CSV 文件<br>涉及知识:请求库 requests、解析库 lxml、Xpath 语法、CSV 文件储存<br>完整代码:<a href="https://github.com/TRHX/Python3-Spider-Practice/tree/master/maoyan-top100" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice/tree/master/maoyan-top100</a><br>其他爬虫实战代码合集(持续更新):<a href="https://github.com/TRHX/Python3-Spider-Practice" target="_blank" rel="noopener">https://github.com/TRHX/Python3-Spider-Practice</a><br>爬虫实战专栏(持续更新):<a href="https://itrhx.blog.csdn.net/article/category/9351278" target="_blank" rel="noopener">https://itrhx.blog.csdn.net/article/category/9351278</a></p> </blockquote> <hr>
Python3 爬虫学习笔记 C18 https://www.itrhx.com/2019/09/21/A50-Python3-spider-C18/ 2019-09-21T03:59:30.358Z 2019-09-24T12:41:19.337Z
Python3 爬虫学习笔记第十八章 —— 【爬虫框架 pyspider — 深入理解】

【18.1】启动参数

常用启动命令:pyspider all,完整命令结构为:pyspider [OPTIONS] COMMAND [ARGS],OPTIONS 为可选参数,包含以下参数:

  • -c, –config FILENAME:指定配置文件名称
  • –logging-config TEXT:日志配置文件名称,默认: pyspider/pyspider/logging.conf
  • –debug:开启调试模式
  • –queue-maxsize INTEGER:队列的最大长度
  • –taskdb TEXT:taskdb 的数据库连接字符串,默认: sqlite
  • –projectdb TEXT:projectdb 的数据库连接字符串,默认: sqlite
  • –resultdb TEXT:resultdb 的数据库连接字符串,默认: sqlite
  • –message-queue TEXT:消息队列连接字符串,默认: multiprocessing.Queue
  • –phantomjs-proxy TEXT:PhantomJS 使用的代理,ip:port 的形式
  • –data-path TEXT:数据库存放的路径
  • –add-sys-path / –not-add-sys-path:将当前工作目录添加到python lib搜索路径
  • –version:显示 pyspider 的版本信息
  • –help:显示帮助信息

配置文件为一个 JSON 文件,一般为 config.json 文件,常用配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"taskdb": "mysql+taskdb://username:password@host:port/taskdb",
"projectdb": "mysql+projectdb://username:password@host:port/projectdb",
"resultdb": "mysql+resultdb://username:password@host:port/resultdb",
"message_queue": "amqp://username:password@host:port/%2F",
"webui": {
"port": 5000,
"username": "some_name",
"password": "some_passwd",
"need-auth": true
}
}

可以设置对应的用户名,密码,端口等信息,使用命令 pyspider -c config.json all 即可运行


【18.2】运行单个组件

pyspider 的架构主要分为 Scheduler(调度器)、Fetcher(抓取器)、Processer(处理器)三个部分,都可以单独运行,基本命令: pyspider [component_name] [options]


【18.2.1】运行 Scheduler

1
pyspider scheduler [OPTIONS]
1
2
3
4
5
6
7
8
9
10
11
12
Options:
--xmlrpc /--no-xmlrpc
--xmlrpc-host TEXT
--xmlrpc-port INTEGER
--inqueue-limit INTEGER 任务队列的最大长度,如果满了则新的任务会被忽略
--delete-time INTEGER 设置为 delete 标记之前的删除时间
--active-tasks INTEGER 当前活跃任务数量配置
--loop-limit INTEGER 单轮最多调度的任务数量
--fail-pause-num INTEGER 上次失败时自动暂停项目暂停次数,任务失败,将0设置为禁用
--scheduler-cls TEXT Scheduler 使用的类
--threads TEXT ThreadBaseScheduler 的线程号,默认值:4
--help 显示帮助信息

【18.2.2】运行 Fetcher

1
pyspider fetcher [OPTIONS]
1
2
3
4
5
6
7
8
9
10
11
12
Options:
--xmlrpc /--no-xmlrpc
--xmlrpc-host TEXT
--xmlrpc-port INTEGER
--poolsize INTEGER 同时请求的个数
--proxy TEXT 使用的代理
--user-agent TEXT 使用的 User-Agent
--timeout TEXT 超时时间
--phantomjs-endpoint TEXT phantomjs 的端点,通过 pyspider 启动 phantomjs
--splash-endpoint TEXT 执行 splash 的端点:http://splash.readthedocs.io/en/stable/api.html execut
--fetcher-cls TEXT Fetcher 使用的类
--help 显示帮助信息

【18.2.3】运行 Processer

1
pyspider processor [OPTIONS]
1
2
3
4
Options:
--processor-cls TEXT Processor 使用的类
--process-time-limit INTEGER 脚本处理时间限制
--help 显示帮助信息

【18.2.4】运行 WebUI

1
pyspider webui [OPTIONS]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Options:
--host TEXT 运行地址
--port INTEGER 运行端口
--cdn TEXT JS 和 CSS 的 CDN 服务器
--scheduler-rpc TEXT Scheduler 的 xmlrpc 路径
--fetcher-rpc TEXT Fetcher 的 xmlrpc 路径
--max-rate FLOAT 每个项目最大的 rate 值
--max-burst FLOAT 每个项目最大的 burst 值
--username TEXT Auth 验证的用户名
--password TEXT Auth 验证的密码
--need-auth 是否需要验证
--webui-instance TEXT 运行时使用的 Flask 应用
--process-time-limit INTEGER 调试中的脚本处理时间限制
--help 显示帮助信息

【18.3】crawl() 方法各参数

参数文档:http://docs.pyspider.org/en/latest/apis/self.crawl/


  • url:爬取目标 URL,可以定义为单个 URL 字符串,也可以定义成 URL 列表

  • callback:回调函数,指定了该 URL 对应的响应内容用哪个方法来解析,示例:
1
2
def on_start(self):
self.crawl('http://www.itrhx.com/', callback=self.index_page)

代码解释:指定 callbackindex_page,代表爬取 http://www.itrhx.com/ 得到的响应会用 index_page() 方法来解析,而 index_page() 方法的第一个参数就是响应对象,如下所示:

1
2
def index_page(self, response):
pass

  • age:任务的有效时间,如果某个任务在有效时间内且已经被执行,则它不会重复执行,有如下两种设置方法:
1
2
def on_start(self):
self.crawl('http://www.itrhx.com/', callback=self.callback, age=10*24*60*60)
1
2
3
@config(age=10 * 24 * 60 * 60)
def callback(self):
pass

  • priority:爬取任务的优先级,其值默认是 0,priority 的数值越大,对应的请求会越优先被调度,如下所示,2.html 页面将会优先爬取:
1
2
3
def index_page(self):
self.crawl('http://www.itrhx.com/1.html', callback=self.index_page)
self.crawl('http://www.itrhx.com/2.html', callback=self.detail_page, priority=1)

  • exetime:设置定时任务,其值是时间戳,默认是 0,即代表立即执行,如下所示表示该任务会在 30 分钟之后执行:
1
2
3
import time
def on_start(self):
self.crawl('http://www.itrhx.com/', callback=self.callback, exetime=time.time()+30*60)

  • retries:定义重试次数,其值默认是 3

  • itag:设置判定网页是否发生变化的节点值,在爬取时会判定次当前节点是否和上次爬取到的节点相同。如果节点相同,则证明页面没有更新,就不会重复爬取,如下所示:
1
2
3
def index_page(self, response):
for item in response.doc('.item').items():
self.crawl(item.find('a').attr.url, callback=self.detail_page, itag=item.find('.update-time').text())

代码解释:设置 update-time 这个节点的值为 itag,在下次爬取时就会首先检测这个值有没有发生变化,如果没有变化,则不再重复爬取,否则执行爬取


  • auto_recrawl:开启时,爬取任务在过期后会重新执行,循环时间即定义的 age 时间长度,如下所示:
1
2
def on_start(self):
self.crawl('http://www.itrhx.com/', callback=self.callback, age=5*60*60, auto_recrawl=True)

代码解释:定义 age 有效期为 5 小时,设置了 auto_recrawlTrue,这样任务就会每 5 小时执行一次


  • method:HTTP 请求方式,默认为 GET,如果想发起 POST 请求,可以将 method 设置为 POST

  • params:定义 GET 请求参数,如下所示表示两个等价的爬取任务:
1
2
3
def on_start(self):
self.crawl('http://httpbin.org/get', callback=self.callback, params={'a': 123, 'b': 'c'})
self.crawl('http://httpbin.org/get?a=123&b=c', callback=self.callback)

  • data:POST 表单数据,当请求方式为 POST 时,我们可以通过此参数传递表单数据,如下所示:
1
2
def on_start(self):
self.crawl('http://httpbin.org/post', callback=self.callback, method='POST', data={'a': 123, 'b': 'c'})

  • files:上传的文件,需要指定文件名,如下所示:
1
2
def on_start(self):
self.crawl('http://httpbin.org/post', callback=self.callback, method='POST', files={field: {filename: 'content'}})

  • user_agent:爬取使用的 User-Agent

  • headers:爬取时使用的 Headers,即 Request Headers

  • cookies:爬取时使用的 Cookies,为字典格式

  • connect_timeout:在初始化连接时的最长等待时间,默认为 20 秒

  • timeout:抓取网页时的最长等待时间,默认为 120 秒

  • allow_redirects:确定是否自动处理重定向,默认为 True

  • validate_cert:确定是否验证证书,此选项对 HTTPS 请求有效,默认为 True

  • proxy:爬取时使用的代理,支持用户名密码的配置,格式为 username:password@hostname:port,如下所示:
1
2
def on_start(self):
self.crawl('http://httpbin.org/get', callback=self.callback, proxy='127.0.0.1:9743')

也可以设置 craw_config 来实现全局配置,如下所示:

1
2
class Handler(BaseHandler):
crawl_config = {'proxy': '127.0.0.1:9743'}

  • fetch_type:开启 PhantomJS 渲染,如果遇到 JavaScript 渲染的页面,指定此字段即可实现 PhantomJS 的对接,pyspider 将会使用 PhantomJS 进行网页的抓取,如下所示:
1
2
def on_start(self):
self.crawl('https://www.taobao.com', callback=self.index_page, fetch_type='js')

  • js_script:页面加载完毕后执行的 JavaScript 脚本,如下所示,页面加载成功后将执行页面混动的 JavaScript 代码,页面会下拉到最底部:
1
2
3
4
5
6
7
def on_start(self):
self.crawl('http://www.example.org/', callback=self.callback,
fetch_type='js', js_script='''
function() {window.scrollTo(0,document.body.scrollHeight);
return 123;
}
''')

  • js_run_at:代表 JavaScript 脚本运行的位置,是在页面节点开头还是结尾,默认是结尾,即 document-end

  • js_viewport_width/js_viewport_height:JavaScript 渲染页面时的窗口大小

  • load_images:在加载 JavaScript 页面时确定是否加载图片,默认为否

  • save:在不同的方法之间传递参数,如下所示:
1
2
3
4
5
6
def on_start(self):
self.crawl('http://www.example.org/', callback=self.callback,
save={'page': 1})

def callback(self, response):
return response.save['page']

  • cancel:取消任务,如果一个任务是 ACTIVE 状态的,则需要将 force_update 设置为 True

  • force_update:即使任务处于 ACTIVE 状态,那也会强制更新状态

【18.4】任务区分

pyspider 判断两个任务是否是重复的是使用的是该任务对应的 URL 的 MD5 值作为任务的唯一 ID,如果 ID 相同,那么两个任务就会判定为相同,其中一个就不会爬取了

某些情况下,请求的链接是同一个,但是 POST 的参数不同,这时可以重写 task_id() 方法,利用 URL 和 POST 的参数来生成 ID,改变这个 ID 的计算方式来实现不同任务的区分:

1
2
3
4
import json
from pyspider.libs.utils import md5string
def get_taskid(self, task):
return md5string(task['url']+json.dumps(task['fetch'].get('data', '')))

【18.5】全局配置

pyspider 可以使用 crawl_config 来指定全局的配置,配置中的参数会和 crawl() 方法创建任务时的参数合并:

1
2
3
4
5
class Handler(BaseHandler):
crawl_config = {
'headers': {'User-Agent': 'GoogleBot',}
'proxy': '127.0.0.1:9743'
}

【18.6】定时爬取

通过 every 属性来设置爬取的时间间隔,如下代码表示每天执行一次爬取:

1
2
3
4
@every(minutes=24 * 60)
def on_start(self):
for url in urllist:
self.crawl(url, callback=self.index_page)

注意事项:如果设置了任务的有效时间(age 参数),因为在有效时间内爬取不会重复,所以要把有效时间设置得比重复时间更短,这样才可以实现定时爬取

错误举例:设定任务的过期时间为 5 天,而自动爬取的时间间隔为 1 天,当第二次尝试重新爬取的时候,pyspider 会监测到此任务尚未过期,便不会执行爬取:

1
2
3
4
5
6
7
@every(minutes=24 * 60)
def on_start(self):
self.crawl('http://www.itrhx.com/', callback=self.index_page)

@config(age=5 * 24 * 60 * 60)
def index_page(self):
pass
]]>
<blockquote> <center><font color="#1BC3FB" size="4">Python3 爬虫学习笔记第十八章 —— 【爬虫框架 pyspider — 深入理解】</font></center> </blockquote>
Python3 爬虫学习笔记 C17 https://www.itrhx.com/2019/09/18/A49-Python3-spider-C17/ 2019-09-18T06:18:23.904Z 2019-09-24T12:41:15.652Z
Python3 爬虫学习笔记第十七章 —— 【爬虫框架 pyspider — 基本使用】

【17.1】初识 pyspider

pyspider 是由国人 Binux 编写的一个 Python 爬虫框架

pyspider 特性:

  • python 脚本控制,可以使用任何 html 解析包(内置 pyquery)
  • WEB 界面编写调试脚本,起停脚本,监控执行状态,查看活动历史,获取结果产出
  • 支持 MySQL、MongoDB、Redis、SQLite、Elasticsearch、PostgreSQL
  • 对接了 PhantomJS,支持抓取 JavaScript 的页面
  • 组件可替换,支持单机和分布式部署,支持 Docker 部署
  • 提供优先级控制、失败重试、定时抓取等功能

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 管理界面


【17.2】使用 pyspider


【17.2.1】主界面

当成功创建了一个爬虫项目后,主界面如下所示:


01
  • 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:查看项目运行结果


【17.2.2】项目界面

创建一个爬虫项目,界面如下所示:


02
  • 创建项目:点击 Create 即可新建一个爬虫项目
  • Project Name:爬虫项目名称
  • Start URL(s) :爬虫入口地址,选填,可在项目中更改

项目创建完成进入调试界面:


03
  • 调试界面右边:编写代码的区域

  • 调试界面左边:调试的区域,用于执行代码,显示输出信息等用途

  • run:单步调试爬虫程序,点击就可运行当前任务

  • < > 箭头:上一步、下一步,用于调试过程中切换到上一步骤或者下一步骤

  • save:保存当前代码,当代码变更后只有保存了再运行才能得到最新结果

  • enable css selector helper: CSS 选择器辅助程序

  • web:页面预览

  • html:可以查看页面源代码

  • follows:表示爬取请求,点击可查看所有的请求

在新建一个爬虫项目的时候,pyspider 已经自动生成了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
# Created on 2019-09-17 21:18:13
# Project: 2

from pyspider.libs.base_handler import *


class Handler(BaseHandler):
crawl_config = {
}

@every(minutes=24 * 60)
def on_start(self):
self.crawl('__START_URL__', callback=self.index_page)

@config(age=10 * 24 * 60 * 60)
def index_page(self, response):
for each in response.doc('a[href^="http"]').items():
self.crawl(each.attr.href, callback=self.detail_page)

@config(priority=2)
def detail_page(self, response):
return {
"url": response.url,
"title": response.doc('title').text(),
}
  • 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} 即可,清除浏览器缓存后就会生效!


【17.3】使用 pyspider 爬取去哪儿网

爬取地址:http://travel.qunar.com/travelbook/list.htm
爬取目标:去哪儿网旅游攻略,发帖作者、标题、正文等


【17.3.1】爬取首页

创建一个名为 qunar 的爬虫项目,Start URL 设置为 http://travel.qunar.com/travelbook/list.htm ,点击 run 出现一个爬取请求


04

左边调试区域出现以下代码:

1
2
3
4
5
6
7
8
{
"process": {
"callback": "on_start"
},
"project": "qunar",
"taskid": "data:,on_start",
"url": "data:,on_start"
}

callback 为 on_start,表示此时执行了 on_start() 方法。在 on_start() 方法中,利用 crawl() 方法即可生成一个爬取请求,点击 index_page 链接后面的箭头会出现许多新的爬取请求,即首页所包含的所有链接


05

此时左边调试区域代码变为:

1
2
3
4
5
6
7
8
9
10
11
12
{
"fetch": {},
"process": {
"callback": "index_page"
},
"project": "qunar",
"schedule": {
"age": 864000
},
"taskid": "73a789f99528a2bdc3ab83a13902962a",
"url": "http://travel.qunar.com/travelbook/list.htm"
}

callback 变为了 index_page,表示此时执行了 index_page() 方法。传入 index_page() 方法的 response 参数为刚才生成的第一个爬取请求的 response 对象,然后调用 doc() 方法,传入提取所有 a 节点的 CSS 选择器,获取 a 节点的属性 href,实现了页面所有链接的提取,随后遍历所有链接,调用 crawl() 方法,把每个链接构造成新的爬取请求,可以看到 follows 新生成了 229 个爬取请求。点击 web 按钮可以直接预览当前页面,点击 html 按钮可以查看此页面源代码


【17.3.2】信息匹配

代码 for each in response.doc('a[href^="http"]').items(): 实现了对整个页面链接的获取,我们需要提取网页的攻略的标题,内容等信息,那么直接替换 doc() 方法里的匹配语句即可,pyspider 提供了非常方便的 CSS 选择器,点击 enable css selector helper 按钮后,选择要匹配的信息并点击,再点击箭头 add to editor 即可得到匹配语句


06

完成了 CSS 选择器的替换,点击 save 保存,再次点击 run 重新执行 index_page() 方法,可以看到 follows 变为了 10 个,即抓取到了 10 篇攻略


【17.3.3】抓取下一页数据

每一页只有 10 篇攻略,想要爬取所有页面的攻略,必须要得到下一页的数据,优化 index_page() 方法:

1
2
3
4
5
6
@config(age=10 * 24 * 60 * 60)
def index_page(self, response):
for each in response.doc('li > .tit > a').items():
self.crawl(each.attr.href, callback=self.detail_page)
next = response.doc('.next').attr.href
self.crawl(next, callback=self.index_page)

匹配下一页按钮,获取下一页按钮的 URL 并赋值给 next,将该 URL 传给 crawl() 方法,指定回调函数为 index_page() 方法,这样会再次调用 index_page() 方法,提取下一页的攻略标题


【17.3.4】抓取JS渲染数据

随便点击一个获取到的攻略,预览该页面,可以观察到头图一直在加载中,切换到 html 查看源代码页面,可以观察到没有 img 节点,那么此处就是后期经过 JavaScript 渲染后才出现的


07

针对 JavaScript 渲染页面,可以通过 PhantomJS 来实现,具体到 pyspider 中,只需要在 index_page()crawl() 抓取方法中添加一个参数 fetch_type 即可:

1
2
3
4
5
6
@config(age=10 * 24 * 60 * 60)
def index_page(self, response):
for each in response.doc('li > .tit > a').items():
self.crawl(each.attr.href, callback=self.detail_page, fetch_type='js')
next = response.doc('.next').attr.href
self.crawl(next, callback=self.index_page)

保存之后再次运行即可看到正常页面


【17.3.5】抓取所有数据

改写 detail_page() 方法,同样通过 CSS 选择器提取 URL、标题、日期、作者、正文、图片等信息:

1
2
3
4
5
6
7
8
9
10
11
@config(priority=2)
def detail_page(self, response):
return {
'url': response.url,
'title': response.doc('#booktitle').text(),
'date': response.doc('.when .data').text(),
'day': response.doc('.howlong .data').text(),
'who': response.doc('.who .data').text(),
'text': response.doc('#b_panel_schedule').text(),
'image': response.doc('.cover_img').attr.src
}

【17.3.6】启动爬虫项目

该爬虫项目完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
# Created on 2019-09-18 09:48:29
# Project: qunar

from pyspider.libs.base_handler import *


class Handler(BaseHandler):
crawl_config = {
}

@every(minutes=24 * 60)
def on_start(self):
self.crawl('http://travel.qunar.com/travelbook/list.htm', callback=self.index_page)

@config(age=10 * 24 * 60 * 60)
def index_page(self, response):
for each in response.doc('li > .tit > a').items():
self.crawl(each.attr.href, callback=self.detail_page, fetch_type='js')
next = response.doc('.next').attr.href
self.crawl(next, callback=self.index_page)

@config(priority=2)
def detail_page(self, response):
return {
'url': response.url,
'title': response.doc('#booktitle').text(),
'date': response.doc('.when .data').text(),
'day': response.doc('.howlong .data').text(),
'who': response.doc('.who .data').text(),
'text': response.doc('#b_panel_schedule').text(),
'image': response.doc('.cover_img').attr.src
}

保存代码后,回到主界面,将项目 status 修改为 RUNNING ,点击 actions 的 run 按钮即可启动爬虫


08

点击 Active Tasks,即可查看最近请求的详细状况:


09

点击 Results,即可查看所有的爬取结果:


10

另外,右上角还可以选择 JSON、CSV 格式

]]>
<blockquote> <center><font color="#1BC3FB" size="4">Python3 爬虫学习笔记第十七章 —— 【爬虫框架 pyspider — 基本使用】</font></center> </blockquote>
Hexo 博客提交百度、谷歌搜索引擎收录 https://www.itrhx.com/2019/09/17/A48-submit-search-engine-inclusion/ 2019-09-17T07:59:46.143Z 2019-12-29T07:20:01.329Z

● 写在前面(必看)

网站在没有提交搜索引擎收录之前,直接搜索你网站的内容是搜不到的,只有提交搜索引擎之后,搜索引擎才能收录你的站点,通过爬虫抓取你网站的东西,对于 hexo 博客来说,如果你是部署在 GitHub Pages,那么你是无法被百度收录的,因为 GitHub 禁止了百度爬虫,最常见的解决办法是双线部署到 Coding Pages 和 GitHub Pages,因为百度爬虫可以爬取到 Coding 上的内容,从而实现百度收录,如果你的 hexo 博客还没有实现双线部署,请参考:《Hexo 双线部署到 Coding Pages 和 GitHub Pages 并实现全站 HPPTS》,另外百度收录的所需的时间较长,大约半个月左右才会看到效果!


● 查看网站是否被收录

首先我们可以输入 site:域名 来查看域名是否被搜索引擎收录,如下图所示,表示没有收录:


01

● 百度资源平台添加网站

访问百度搜索资源平台官网,注册或者登陆百度账号,依次选择【用户中心】-【站点管理】,添加你的网站,在添加站点时会让你选择协议头(http 或者 https),如果选择 https,它会验证你的站点,大约能在一天之内完成,我的网站已经实现了全站 https,因此选择了 https 协议,但是不知道为什么始终验证失败,实在是无解,只能选择 http 协议了,如果你的站点也实现了全站 https,也可以尝试一下


02

之后会让你验证网站所有权,提供三种验证方式:

  • 文件验证:下载给定的文件,将其放到本地主题目录 source 文件夹,然后部署上去完成验证
  • HTML 标签验证:一般是给一个 meta 标签,放到首页 <head></head> 标签之间即可完成验证
  • CNAME 验证:个人觉得这种方法最简单,去域名 DNS 添加一个 CNAME 记录即可完成验证

03

04

● 提交百度搜索

百度提供了自动提交和手动提交两种方式,其中自动提交又分为主动推送、自动推送和 sitemap 三种方式,以下是官方给出的解释:

  • 主动推送:最为快速的提交方式,推荐您将站点当天新产出链接立即通过此方式推送给百度,以保证新链接可以及时被百度收录

  • 自动推送:是轻量级链接提交组件,将自动推送的 JS 代码放置在站点每一个页面源代码中,当页面被访问时,页面链接会自动推送给百度,有利于新页面更快被百度发现

  • sitemap:您可以定期将网站链接放到sitemap中,然后将sitemap提交给百度。百度会周期性的抓取检查您提交的sitemap,对其中的链接进行处理,但收录速度慢于主动推送

  • 手动提交:如果您不想通过程序提交,那么可以采用此种方式,手动将链接提交给百度

四种提交方式对比:

方式主动推送自动推送Sitemap手动提交
速度最快——————
开发成本不需开发
可提交量
是否建议提交历史连接
和其他提交方法是否有冲突

个人推荐同时使用主动推送和 sitemap 方式,下面将逐一介绍这四种提交方式的具体实现方法


● 主动推送

在博客根目录安装插件 npm install hexo-baidu-url-submit --save,然后在根目录 _config.yml 文件里写入以下配置:

1
2
3
4
5
baidu_url_submit:
count: 1 # 提交最新的多少个链接
host: www.itrhx.com # 在百度站长平台中添加的域名
token: your_token # 秘钥
path: baidu_urls.txt # 文本文档的地址, 新链接会保存在此文本文档里

其中的 token 可以在【链接提交】-【自动提交】-【主动推送】下面看到,接口调用地址最后面 token=xxxxx 即为你的 token


05

同样是在根目录的 _config.yml 文件,大约第 17 行处,url 要改为在百度站长平台添加的域名,也就是你网站的首页地址:

1
2
3
4
# URL
url: https://www.itrhx.com
root: /
permalink: :year/:month/:day/:title/

最后,加入新的 deployer:

1
2
3
4
5
6
7
8
9
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
- type: git
repository:
github: git@github.com:TRHX/TRHX.github.io.git # 这是原来的 github 配置
coding: git@git.dev.tencent.com:TRHX/TRHX.git # 这是原来的 coding 配置
branch: master
- type: baidu_url_submitter # 这是新加的主动推送

最后执行 hexo g -d 部署一遍即可实现主动推送,推送成功的标志是:在执行部署命令最后会显示类似如下代码:

1
2
{"remain":4999953,"success":47}
INFO Deploy done: baidu_url_submitter

这表示有 47 个页面已经主动推送成功,remain 的意思是当天剩余的可推送 url 条数

主动推送相关原理介绍:

  • 新链接的产生:hexo generate 会产生一个文本文件,里面包含最新的链接
  • 新链接的提交:hexo deploy 会从上述文件中读取链接,提交至百度搜索引擎

该插件的 GitHub 地址:https://github.com/huiwang/hexo-baidu-url-submit


● 自动推送

关于自动推送百度官网给出的解释是:自动推送是百度搜索资源平台为提高站点新增网页发现速度推出的工具,安装自动推送JS代码的网页,在页面被访问时,页面URL将立即被推送给百度


06

此时要注意,有些 hexo 主题集成了这项功能,比如 next 主题,在 themes\next\layout_scripts\ 下有个 baidu_push.swig 文件,我们只需要把如下代码粘贴到该文件,然后在主题配置文件设置 baidu_push: true 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{% if theme.baidu_push %}
<script>
(function(){
var bp = document.createElement('script');
var curProtocol = window.location.protocol.split(':')[0];
if (curProtocol === 'https') {
bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
}
else {
bp.src = 'http://push.zhanzhang.baidu.com/push.js';
}
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(bp, s);
})();
</script>
{% endif %}

然而大部分主题是没有集成这项功能的,对于大部分主题来说,我们可以把以下代码粘贴到 head.ejs 文件的 <head></head> 标签之间即可,从而实现自动推送(比如我使用的是 Material X 主题,那么只需要把代码粘贴到 \themes\material-x\layout\_partial\head.ejs 中即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
(function(){
var bp = document.createElement('script');
var curProtocol = window.location.protocol.split(':')[0];
if (curProtocol === 'https') {
bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
}
else {
bp.src = 'http://push.zhanzhang.baidu.com/push.js';
}
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(bp, s);
})();
</script>

● sitemap

首先我们要使用以下命令生成一个网站地图:

1
2
npm install hexo-generator-sitemap --save     
npm install hexo-generator-baidu-sitemap --save

这里也注意一下,将根目录的 _config.yml 文件,大约第 17 行处,url 改为在百度站长平台添加的域名,也就是你网站的首页地址:

1
2
3
4
# URL
url: https://www.itrhx.com
root: /
permalink: :year/:month/:day/:title/

然后使用命令 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


07

● 手动提交

手动提交不需要其他额外操作,直接把需要收录的页面的 url 提交即可,这种方法效率较低,更新较慢,不推荐使用


08

● 提交谷歌搜索

提交谷歌搜索引擎比较简单,在提交之前,我们依然可以使用 site:域名 查看网站是否被收录,我的网站搭建了有差不多一年了,之前也没提交过收录,不过谷歌爬虫的确是强大,即使没有提交过,现在也能看到有一百多条结果了:


09

接下来我们将网站提交谷歌搜索引擎搜索,进入谷歌站长平台,登录你的谷歌账号之后会让你验证网站所有权:


10

有两种验证方式,分别是网域和网址前缀,两种资源类型区别如下:

网址前缀资源
网域资源
说明仅包含具有指定前缀(包括协议 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 记录,点击验证即可


11

提交谷歌收录比较简单,选择站点地图,将我们之前生成的 sitemap 提交就行了,过几分钟刷新一下看到成功字样表示提交成功!


12
]]>
网站 SEO 优化,Hexo 博客提交百度、谷歌搜索引擎收录
Hexo 双线部署到 Coding Pages 和 GitHub Pages 并实现全站 HPPTS https://www.itrhx.com/2019/09/16/A47-hexo-deployed-to-github-and-coding/ 2019-09-16T06:11:40.959Z 2019-12-29T07:20:11.697Z

部署到 Coding Pages 的好处:国内访问速度更快,可以提交百度收录(GitHub 禁止了百度的爬取)

部署到 Coding Pages 的坏处:就今年来说,Coding 不太稳定,随时有宕机的可能,群里的朋友已经经历过几次了,不过相信以后会越来越稳定的

部署过程中常见的问题:无法实现全站 HTTPS,Coding 申请 SSL 证书失败,浏览器可能会提示不是安全链接

本文前提:你已经将 Hexo 成功部署到了 GitHub Pages,如果还没有,请参考:《使用Github Pages和Hexo搭建自己的独立博客【超级详细的小白教程】》

本文将全面讲述如何成功双线部署到 Coding Pages 和 GitHub Pages 并实现全站 HPPTS,同时解决一些常见的问题!


1.创建项目

进入 Coding 官网,点击个人版登陆,没有账号就注册一个并登录,由于 Coding 已经被腾讯收购了,所以登录就会来到腾讯云开发者平台,点击创建项目


01

项目名称建议和你的用户名一致,这样做的好处是:到时候可以直接通过 user_name.coding.me 访问你的博客,如果项目名与用户名不一致,则需要通过 user_name.coding.me/project_name 才能访问,项目描述可以随便写


02

2.配置公钥

配置 SSH 公钥方法与 GitHub Pages 的方式差不多,点击你的头像,依次选择【个人设置】-【SSH公钥】-【新增公钥】


03

前面部署到 GitHub Pages 的时候就已经有了一对公钥,我们直接将该公钥粘贴进去就行,公钥名称可以随便写,选中永久有效选项

PS:公钥储存位置一般在 C:\Users\用户名\.ssh 目录下的 id_rsa.pub 文件里,用记事本打开复制其内容即可


04

添加公钥后,我们可以右键 Get Bash,输入以下命令来检查是否配置成功:

1
ssh -T git@git.coding.net

若出现以下提示,则证明配置成功:

1
2
Coding 提示: Hello XXX, You've connected to Coding.net via SSH. This is a personal key.
XXX,你好,你已经通过 SSH 协议认证 Coding.net 服务,这是一个个人公钥

3.配置 _config.yml

进入你的项目,在右下角有选择连接方式,选择 SSH 方式(HTTPS 方式也可以,但是这种方式有时候可能连接不上,SSH 连接不容易出问题),一键复制,然后打开你本地博客根目录的 _config.yml 文件,找到 deploy 关键字,添加 coding 地址:coding: git@git.dev.tencent.com:user_name/user_name.git,也就是刚刚复制的 SSH 地址


05

06

添加完成后先执行命令 hexo clean 清理一下缓存,然后执行命令 hexo g -d 将博客双线部署到 Coding Pages 和 GitHub Pages,如下图所示表示部署成功:


13

4.开启 Coding Pages

进入你的项目,在代码栏下选择 Pages 服务,一键开启 Coding Pages,等待几秒后刷新网页即可看到已经开启的 Coding Pages,到目前为止,你就可以通过 xxxx.coding.me(比如我的是 trhx.coding.me)访问你的 Coding Pages 页面了


07

08

5.绑定域名并开启 HPPTS

首先在你的域名 DNS 设置中添加一条 CNAME 记录指向 xxxx.coding.me,解析路线选择 默认,将 GitHub 的解析路线改为 境外,这样境外访问就会走 GitHub,境内就会走 Coding,也有人说阿里云是智能解析,自动分配路线,如果解析路线都是默认,境外访问同样会智能选择走 GitHub,境内走 Coding,我没有验证过,有兴趣的可以自己试试,我的解析如下图所示:


09

然后点击静态 Pages 应用右上角的设置,进入设置页面,这里要注意,如果你之前已经部署到了 GitHub Pages 并开启了 HTTPS,那么直接在设置页面绑定你自己的域名,SSL/TLS 安全证书就会显示申请错误,如下图所示,没有申请到 SSL 证书,当你访问你的网站时,浏览器就会提示不是安全连接


10

申请错误原因是:在验证域名所有权时会定位到 Github Pages 的主机上导致 SSL 证书申请失败

正确的做法是:先去域名 DNS 把 GitHub 的解析暂停掉,然后再重新申请 SSL 证书,大约十秒左右就能申请成功,然后开启强制 HTTPS 访问

这里也建议同时绑定有 www 前缀和没有 www 前缀的,如果要绑定没有 www 前缀的,首先要去域名 DNS 添加一个 A 记录,主机记录为 @,记录值为你博客 IP 地址,IP 地址可以在 cmd 命令行 ping 一下得到,然后在 Coding Pages 中设置其中一个为【首选】,另一个设置【跳转至首选】,这样不管用户是否输入 www 前缀都会跳到有 www 前缀的了

在博客资源引用的时候也要注意所有资源的 URL 必须是以 https:// 开头,不然浏览器依旧会提示不安全!


13

11_1

至此,我们的 Hexo 博客就成功双线部署到 Coding Pages 和 GitHub Pages 了,并且也实现了全站 HPPTS,最后来一张 GitHub Pages 和 Coding Pages 在国内的速度对比图,可以明显看到速度的提升


12
]]>
Hexo 双线部署到 Coding Pages 和 GitHub Pages 并实现全站 HPPTS
Python3 爬虫学习笔记 C16 https://www.itrhx.com/2019/09/14/A46-Python3-spider-C16/ 2019-09-13T16:44:50.577Z 2019-09-24T12:43:19.863Z
Python3 爬虫学习笔记第十六章 —— 【数据储存系列 — Redis】

【16.1】关于 Redis

Redis 是一个基于内存的高效的键值型(key-value)非关系型数据库,它支持存储的 value 类型非常多,包括 string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合) 和 hash(哈希类型),它的性能十分优越,可以支持每秒十几万此的读/写操作,其性能远超数据库,并且还支持集群、分布式、主从同步等配置,原则上可以无限扩展,让更多的数据存储在内存中,此外,它还支持一定的事务能力,这保证了高并发的场景下数据的安全和一致性。


【16.2】使用 Redis

首先安装 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
2
3
4
5
from redis import StrictRedis

redis = StrictRedis(host='localhost', port=6379, db=0, password='000000')
redis.set('name', 'TRHX')
print(redis.get('name'))

传入 Redis 的地址、运行端口、使用的数据库和密码, 4 个参数默认值分别为 localhost、6379、0 和 None,声明一个 StrictRedis 对象,调用 set() 方法,设置一个键值对,输出结果如下:

1
b'TRHX'

另外也可以使用 ConnectionPool 来连接:

1
2
3
4
from redis import StrictRedis, ConnectionPool  

pool = ConnectionPool(host='localhost', port=6379, db=0, password='000000')
redis = StrictRedis(connection_pool=pool)

ConnectionPool 也支持通过 URL 来构建:

1
2
3
redis://[:password]@host:port/db  # 创建 Redis TCP 连接
rediss://[:password]@host:port/db # 创建 Redis TCP+SSL 连接
unix://[:password]@/path/to/socket.sock?db=db # 创建 Redis UNIX socket 连接

代码示例:

1
2
3
4
5
from redis import StrictRedis, ConnectionPool

url = 'redis://:000000@localhost:6379/0'
pool = ConnectionPool.from_url(url)
redis = StrictRedis(connection_pool=pool)

以下是有关的键操作、字符串操作、列表操作、集合操作、散列操作的各种方法,记录一下,方便查阅
来源:《Python3 网络爬虫开发实战(崔庆才著)》
Redis 命令参考:http://redisdoc.com/http://doc.redisfans.com/


【16.3】Key(键)操作

方法作用参数说明示例示例说明示例结果
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 重命名为 nicknameTrue
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

【16.4】String(字符串)操作

方法作用参数说明示例示例说明示例结果
set(name, value)给数据库中键名为 name 的 string 赋予值 valuename:键名;value:值redis.set(‘name’, ‘Bob’)给 name 这个键的 value 赋值为 BobTrue
get(name)返回数据库中键名为 name 的 string 的 valuename:键名redis.get(‘name’)返回 name 这个键的 valueb’Bob’
getset(name, value)给数据库中键名为 name 的 string 赋予值 value 并返回上次的 valuename:键名;value:新值redis.getset(‘name’, ‘Mike’)赋值 name 为 Mike 并得到上次的 valueb’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 的位置补 World11,修改后的字符串长度
mset(mapping)批量赋值mapping:字典或关键字参数redis.mset({‘name1’: ‘Durant’, ‘name2’: ‘James’})将 name1 设为 Durant,name2 设为 JamesTrue
msetnx(mapping)键均不存在时才批量赋值mapping:字典或关键字参数redis.msetnx({‘name3’: ‘Smith’, ‘name4’: ‘Curry’})在 name3 和 name4 均不存在的情况下才设置二者值True
incr(name, amount=1)键名为 name 的 value 增值操作,默认为 1,键不存在则被创建并设为 amountname:键名;amount:增长的值redis.incr(‘age’, 1)age 对应的值增 1,若不存在,则会创建并设置为 11,即修改后的值
decr(name, amount=1)键名为 name 的 value 减值操作,默认为 1,键不存在则被创建并将 value 设置为 - amountname:键名;amount:减少的值redis.decr(‘age’, 1)age 对应的值减 1,若不存在,则会创建并设置为-1-1,即修改后的值
append(key, value)键名为 key 的 string 的值附加 valuekey:键名redis.append(‘nickname’, ‘OK’)向键名为 nickname 的值后追加 OK13,即修改后的字符串长度
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

【16.5】Hash(哈希表)操作

方法作用参数说明示例示例说明示例结果
hset(name, key, value)向键名为 name 的散列表中添加映射name:键名;key:映射键名;value:映射键值hset(‘price’, ‘cake’, 5)向键名为 price 的散列表中添加映射关系,cake 的值为 51,即添加的映射个数
hsetnx(name, key, value)如果映射键名不存在,则向键名为 name 的散列表中添加映射name:键名;key:映射键名;value:映射键值hsetnx(‘price’, ‘book’, 6)向键名为 price 的散列表中添加映射关系,book 的值为 61,即添加的映射个数
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 的散列表中映射的值增加 amountname:键名;key:映射键名;amount:增长量redis.hincrby(‘price’, ‘apple’, 3)key 为 price 的散列表中 apple 的值增加 36,修改后的值
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’}

【16.6】List(列表)操作

方法作用参数说明示例示例说明示例结果
rpush(name, *values)在键名为 name 的列表末尾添加值为 value 的元素,可以传多个name:键名;values:值redis.rpush(‘list’, 1, 2, 3)向键名为 list 的列表尾添加 1、2、33,列表大小
lpush(name, *values)在键名为 name 的列表头添加值为 value 的元素,可以传多个name:键名;values:值redis.lpush(‘list’, 0)向键名为 list 的列表头部添加 04,列表大小
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 的位置赋值为 5True
lrem(name, count, value)删除 count 个键的列表中值为 value 的元素name:键名;count:删除个数;value:值redis.lrem(‘list’, 2, 3)将键名为 list 的列表删除两个 31,即删除的个数
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:目标列表的 keyredis.rpoplpush(‘list’, ‘list2’)将键名为 list 的列表尾元素删除并将其添加到键名为 list2 的列表头部,然后返回b’2’

【16.7】Set(集合)操作

方法作用参数说明示例示例说明示例结果
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 的集合中删除 Book1,即删除的数据个数
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 的集合的交集并将其保存为 inttag1
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 的集合的并集并将其保存为 inttag3
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 的集合的差集并将其保存为 inttag3
smembers(name)返回键名为 name 的集合的所有元素name:键名redis.smembers(‘tags’)返回键名为 tags 的集合的所有元素{b’Pen’, b’Book’, b’Coffee’}
srandmember(name)随机返回键名为 name 的集合中的一个元素,但不删除元素name:键值redis.srandmember(‘tags’)随机返回键名为 tags 的集合中的一个元素Srandmember (name)

【16.8】SortedSet(有序集合)操作

方法作用参数说明示例示例说明示例结果
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 中删除 Mike1,即删除的元素个数
zincrby(name, value, amount=1)如果在键名为 name 的 zset 中已经存在元素 value,则将该元素的 score 增加 amount;否则向该集合中添加该元素,其 score 的值为 amountname:键名;value:元素;amount:增长的 score 值redis.zincrby(‘grade’, ‘Bob’, -2)键名为 grade 的 zset 中 Bob 的 score 减 298.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:是否带 scoreredis.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:是否带 scoreredis.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:最高 scoreredis.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:最高 scoreredis.zremrangebyscore (‘grade’, 80, 90)删除 score 在 80 到 90 之间的元素1,即删除的元素个数

【16.9】RedisDump

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 命令了


【16.9.1】导出数据 redis-dump

在命令行输入 redis-dump -h 可以查看:

1
2
3
4
5
6
7
8
9
10
11
12
Usage: E:/Ruby26-x64/bin/redis-dump [global options] COMMAND [command options]
-u, --uri=S Redis URI (e.g. redis://hostname[:port])
-d, --database=S Redis database (e.g. -d 15)
-a, --password=S Redis password (e.g. -a 'my@pass/word')
-s, --sleep=S Sleep for S seconds after dumping (for debugging)
-c, --count=S Chunk size (default: 10000)
-f, --filter=S Filter selected keys (passed directly to redis' KEYS command)
-b, --base64 Encode key values as base64 (useful for binary values)
-O, --without_optimizations Disable run time optimizations
-V, --version Display version
-D, --debug
--nosafe

命令解释:

  • -u Redis 连接字符串
  • -d 数据库代号
  • -a 数据库密码
  • -s 导出之后的休眠时间
  • -c 分块大小,默认是 10000
  • -f 导出时的过滤器
  • -b 将键值编码为 base64(对二进制值有用)
  • -O 禁用运行时优化
  • -V 显示版本
  • -D 开启调试

导出数据示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
redis-dump

# 指定端口
redis-dump -u 127.0.0.1:6379

# 指定端口和密码
redis-dump -u :password@127.0.0.1:6379

# 导出指定数据库
redis-dump -u 127.0.0.1:6379 -d 3

# 导出包含特定值的数据
redis-dump -u 127.0.0.1:6379 -f age

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 导出所有数据
{"db":0,"key":"name5","ttl":-1,"type":"string","value":"DDD","size":3}
{"db":0,"key":"name2","ttl":-1,"type":"string","value":"AAA","size":3}
{"db":0,"key":"name4","ttl":-1,"type":"string","value":"CCC","size":3}
{"db":0,"key":"name6","ttl":-1,"type":"string","value":"CCC","size":3}
{"db":0,"key":"name","ttl":-1,"type":"string","value":"TRHX","size":4}
{"db":0,"key":"name3","ttl":-1,"type":"string","value":"BBB","size":3}
{"db":1,"key":"name2","ttl":-1,"type":"string","value":"BBB","size":3}
{"db":1,"key":"name1","ttl":-1,"type":"string","value":"AAA","size":3}
{"db":2,"key":"name2","ttl":-1,"type":"string","value":"BBB","size":3}
{"db":2,"key":"name1","ttl":-1,"type":"string","value":"AAA","size":3}
{"db":3,"key":"name2","ttl":-1,"type":"string","value":"HHH","size":3}
{"db":3,"key":"name1","ttl":-1,"type":"string","value":"RRR","size":3}
{"db":4,"key":"age","ttl":-1,"type":"string","value":"20","size":2}
{"db":4,"key":"age2","ttl":-1,"type":"string","value":"19","size":2}

# 导出 3 号数据库
{"db":3,"key":"name2","ttl":-1,"type":"string","value":"HHH","size":3}
{"db":3,"key":"name1","ttl":-1,"type":"string","value":"RRR","size":3}

# 导出 key 包含 age 的数据
{"db":4,"key":"age","ttl":-1,"type":"string","value":"20","size":2}
{"db":4,"key":"age2","ttl":-1,"type":"string","value":"19","size":2}

导出所有数据为 JSON 文件:

1
redis-dump -u 127.0.0.1:6379 > db_full.json

该命令将会在当前目录生成一个名为 db_full.json 的文件,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{"db":0,"key":"name5","ttl":-1,"type":"string","value":"DDD","size":3}
{"db":0,"key":"name2","ttl":-1,"type":"string","value":"AAA","size":3}
{"db":0,"key":"name4","ttl":-1,"type":"string","value":"CCC","size":3}
{"db":0,"key":"name6","ttl":-1,"type":"string","value":"CCC","size":3}
{"db":0,"key":"name","ttl":-1,"type":"string","value":"TRHX","size":4}
{"db":0,"key":"name3","ttl":-1,"type":"string","value":"BBB","size":3}
{"db":1,"key":"name2","ttl":-1,"type":"string","value":"BBB","size":3}
{"db":1,"key":"name1","ttl":-1,"type":"string","value":"AAA","size":3}
{"db":2,"key":"name2","ttl":-1,"type":"string","value":"BBB","size":3}
{"db":2,"key":"name1","ttl":-1,"type":"string","value":"AAA","size":3}
{"db":3,"key":"name2","ttl":-1,"type":"string","value":"HHH","size":3}
{"db":3,"key":"name1","ttl":-1,"type":"string","value":"RRR","size":3}
{"db":4,"key":"age","ttl":-1,"type":"string","value":"20","size":2}
{"db":4,"key":"age2","ttl":-1,"type":"string","value":"19","size":2}

使用参数 -d 指定某个数据库的所有数据导出为 JSON 文件:

1
redis-dump -u 127.0.0.1:6379 -d 4 > db_db4.json

该命令会将 4 号数据库的数据导出到 db_db4.json 文件:

1
2
{"db":4,"key":"age","ttl":-1,"type":"string","value":"20","size":2}
{"db":4,"key":"age2","ttl":-1,"type":"string","value":"19","size":2}

使用参数 -f 过滤数据,只导出特定的数据:

1
redis-dump -u 127.0.0.1:6379 -f name > db_name.json

该命令会导出 key 包含 name 的数据到 db_name.json 文件:

1
2
3
4
5
6
7
8
9
10
11
12
{"db":0,"key":"name5","ttl":-1,"type":"string","value":"DDD","size":3}
{"db":0,"key":"name2","ttl":-1,"type":"string","value":"AAA","size":3}
{"db":0,"key":"name4","ttl":-1,"type":"string","value":"CCC","size":3}
{"db":0,"key":"name6","ttl":-1,"type":"string","value":"CCC","size":3}
{"db":0,"key":"name","ttl":-1,"type":"string","value":"TRHX","size":4}
{"db":0,"key":"name3","ttl":-1,"type":"string","value":"BBB","size":3}
{"db":1,"key":"name2","ttl":-1,"type":"string","value":"BBB","size":3}
{"db":1,"key":"name1","ttl":-1,"type":"string","value":"AAA","size":3}
{"db":2,"key":"name2","ttl":-1,"type":"string","value":"BBB","size":3}
{"db":2,"key":"name1","ttl":-1,"type":"string","value":"AAA","size":3}
{"db":3,"key":"name2","ttl":-1,"type":"string","value":"HHH","size":3}
{"db":3,"key":"name1","ttl":-1,"type":"string","value":"RRR","size":3}

【16.9.2】导入数据 redis-load

在命令行输入 redis-load -h 可以查看:

1
2
3
4
5
6
7
8
9
redis-load --help  
Try: redis-load [global options] COMMAND [command options]
-u, --uri=S Redis URI (e.g. redis://hostname[:port])
-d, --database=S Redis database (e.g. -d 15)
-s, --sleep=S Sleep for S seconds after dumping (for debugging)
-n, --no_check_utf8
-V, --version Display version
-D, --debug
--nosafe

命令解释:

  • -u Redis 连接字符串
  • -d 数据库代号,默认是全部
  • -s 导出之后的休眠时间
  • -n 不检测 UTF-8 编码
  • -V 显示版本
  • -D 开启调试

导入示例:

1
2
3
4
5
# 将 test.json 文件所有内容导入到数据库
< test.json redis-load -u 127.0.0.1:6379

# 将 test.json 文件 db 值为 6 的数据导入到数据库
< test.json redis-load -u 127.0.0.1:6379 -d 6

另外,以下方法也能导入数据:

1
2
3
4
5
# 将 test.json 文件所有内容导入到数据库
cat test.json | redis-load -u 127.0.0.1:6379

# 将 test.json 文件 db 值为 6 的数据导入到数据库
cat test.json | redis-load -u 127.0.0.1:6379 -d 6

注意:cat 是 Linux 系统专有的命令,在 Windows 系统里没有 cat 这个命令,可以使用 Windows 批处理命令 type 代替 cat

]]>
<blockquote> <center><font color="#1BC3FB" size="4">Python3 爬虫学习笔记第十六章 —— 【数据储存系列 — Redis】</font></center> </blockquote>
Python3 爬虫学习笔记 C15 https://www.itrhx.com/2019/09/10/A45-Python3-spider-C15/ 2019-09-10T11:46:13.293Z 2019-09-24T12:41:02.822Z
Python3 爬虫学习笔记第十五章 —— 【代理的基本使用】

【15.1】代理初识

大多数网站都有反爬虫机制,如果一段时间内同一个 IP 发送的请求过多,服务器就会拒绝访问,直接禁封该 IP,此时,设置代理即可解决这个问题,网络上有许多免费代理和付费代理,比如西刺代理全网代理 IP快代理等,设置代理需要用到的就是代理 IP 地址和端口号,如果电脑上装有代理软件(例如:酸酸乳SSR),软件一般会在本机创建 HTTP 或 SOCKS 代理服务,直接使用此代理也可以

【15.2】urllib 库使用代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from urllib.error import URLError
from urllib.request import ProxyHandler, build_opener

proxy = '127.0.0.1:1080'
proxy_handler = ProxyHandler({
'http': 'http://' + proxy,
'https': 'https://' + proxy
})
opener = build_opener(proxy_handler)
try:
response = opener.open('http://httpbin.org/get')
print(response.read().decode('utf8'))
except URLError as e:
print(e.reason)

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
2
3
4
5
6
7
8
9
10
{
"args": {},
"headers": {
"Accept-Encoding": "identity",
"Host": "httpbin.org",
"User-Agent": "Python-urllib/3.6"
},
"origin": "168.70.60.141, 168.70.60.141",
"url": "https://httpbin.org/get"
}

如果是需要认证的代理,只需要在代理前面加入代理认证的用户名密码即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from urllib.error import URLError
from urllib.request import ProxyHandler, build_opener

proxy = 'username:password@127.0.0.1:1080'
proxy_handler = ProxyHandler({
'http': 'http://' + proxy,
'https': 'https://' + proxy
})
opener = build_opener(proxy_handler)
try:
response = opener.open('http://httpbin.org/get')
print(response.read().decode('utf8'))
except URLError as e:
print(e.reason)

如果代理是 SOCKS5 类型,需要用到 socks 模块,设置代理方法如下:

扩展:SOCKS5 是一个代理协议,它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色,使得内部网中的前端机器变得能够访问 Internet 网中的服务器,或者使通讯更加安全

1
2
3
4
5
6
7
8
9
10
11
12
import socks
import socket
from urllib import request
from urllib.error import URLError

socks.set_default_proxy(socks.SOCKS5, '127.0.0.1', 1080)
socket.socket = socks.socksocket
try:
response = request.urlopen('http://httpbin.org/get')
print(response.read().decode('utf-8'))
except URLError as e:
print(e.reason)

【15.3】requests 库使用代理

requests 库使用代理只需要传入 proxies 参数即可:

1
2
3
4
5
6
7
8
9
10
11
12
import requests

proxy = '127.0.0.1:1080'
proxies = ({
'http': 'http://' + proxy,
'https': 'https://' + proxy
})
try:
response = requests.get('http://httpbin.org/get', proxies=proxies)
print(response.text)
except requests.exceptions.ChunkedEncodingError as e:
print('Error', e.args)

输出结果:

1
2
3
4
5
6
7
8
9
10
11
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.22.0"
},
"origin": "168.70.60.141, 168.70.60.141",
"url": "https://httpbin.org/get"
}

同样的,如果是需要认证的代理,也只需要在代理前面加入代理认证的用户名密码即可:

1
2
3
4
5
6
7
8
9
10
11
12
import requests

proxy = 'username:password@127.0.0.1:1080'
proxies = ({
'http': 'http://' + proxy,
'https': 'https://' + proxy
})
try:
response = requests.get('http://httpbin.org/get', proxies=proxies)
print(response.text)
except requests.exceptions.ChunkedEncodingError as e:
print('Error', e.args)

如果代理是 SOCKS5 类型,需要用到 requests[socks] 模块或者 socks 模块,使用 requests[socks] 模块时设置代理方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
import requests

proxy = '127.0.0.1:1080'
proxies = {
'http': 'socks5://' + proxy,
'https': 'socks5://' + proxy
}
try:
response = requests.get('http://httpbin.org/get', proxies=proxies)
print(response.text)
except requests.exceptions.ConnectionError as e:
print('Error', e.args)

使用 socks 模块时设置代理方法如下(此类方法为全局设置):

1
2
3
4
5
6
7
8
9
10
11
import requests
import socks
import socket

socks.set_default_proxy(socks.SOCKS5, '127.0.0.1', 1080)
socket.socket = socks.socksocket
try:
response = requests.get('http://httpbin.org/get')
print(response.text)
except requests.exceptions.ConnectionError as e:
print('Error', e.args)

【15.4】Selenium 使用代理

【15.4.1】Chrome

1
2
3
4
5
6
7
8
from selenium import webdriver

proxy = '127.0.0.1:1080'
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--proxy-server=http://' + proxy)
path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe'
browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
browser.get('http://httpbin.org/get')

通过 ChromeOptions 来设置代理,在创建 Chrome 对象的时候用 chrome_options 参数传递即可,访问目标链接后显示如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Host": "httpbin.org",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"
},
"origin": "168.70.60.141, 168.70.60.141",
"url": "https://httpbin.org/get"
}

如果是认证代理,则设置方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import zipfile

ip = '127.0.0.1'
port = 1080
username = 'username'
password = 'password'

manifest_json = """{"version":"1.0.0","manifest_version": 2,"name":"Chrome Proxy","permissions": ["proxy","tabs","unlimitedStorage","storage","<all_urls>","webRequest","webRequestBlocking"],"background": {"scripts": ["background.js"]
}
}
"""

background_js ="""
var config = {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: "http",
host: "%(ip) s",
port: %(port) s
}
}
}

chrome.proxy.settings.set({value: config, scope: "regular"}, function() {});

function callbackFn(details) {
return {
authCredentials: {username: "%(username) s",
password: "%(password) s"
}
}
}

chrome.webRequest.onAuthRequired.addListener(
callbackFn,
{urls: ["<all_urls>"]},
['blocking']
)
""" % {'ip': ip, 'port': port, 'username': username, 'password': password}

plugin_file = 'proxy_auth_plugin.zip'
with zipfile.ZipFile(plugin_file, 'w') as zp:
zp.writestr("manifest.json", manifest_json)
zp.writestr("background.js", background_js)
chrome_options = Options()
chrome_options.add_argument("--start-maximized")
path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe'
chrome_options.add_extension(plugin_file)
browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
browser.get('http://httpbin.org/get')

需要在本地创建一个 manifest.json 配置文件和 background.js 脚本来设置认证代理。运行代码之后本地会生成一个 proxy_auth_plugin.zip 文件来保存当前配置

【15.4.1】PhantomJS

借助 service_args 参数,也就是命令行参数即可设置代理:

1
2
3
4
5
6
7
8
9
10
from selenium import webdriver

service_args = [
'--proxy=127.0.0.1:1080',
'--proxy-type=http'
]
path = r'F:\PycharmProjects\Python3爬虫\phantomjs-2.1.1\bin\phantomjs.exe'
browser = webdriver.PhantomJS(executable_path=path, service_args=service_args)
browser.get('http://httpbin.org/get')
print(browser.page_source)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
<html><head></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,en,*",
"Host": "httpbin.org",
"User-Agent": "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1"
},
"origin": "168.70.60.141, 168.70.60.141",
"url": "https://httpbin.org/get"
}
</pre></body></html>

如果是需要认证的代理,只需要在 service_args 参数加入 –proxy-auth 选项即可:

1
2
3
4
5
6
7
8
9
10
11
from selenium import webdriver

service_args = [
'--proxy=127.0.0.1:1080',
'--proxy-type=http',
'--proxy-auth=username:password'
]
path = r'F:\PycharmProjects\Python3爬虫\phantomjs-2.1.1\bin\phantomjs.exe'
browser = webdriver.PhantomJS(executable_path=path, service_args=service_args)
browser.get('http://httpbin.org/get')
print(browser.page_source)
]]>
<blockquote> <center><font color="#1BC3FB" size="4">Python3 爬虫学习笔记第十五章 —— 【代理的基本使用】</font></center> </blockquote>
Python3 爬虫学习笔记 C14 https://www.itrhx.com/2019/09/08/A44-Python3-spider-C14/ 2019-09-07T17:38:41.491Z 2019-09-24T12:43:31.445Z
Python3 爬虫学习笔记第十四章 —— 【验证码对抗系列 — 点触验证码】

【14.1】关于点触验证码

点触验证码是由杭州微触科技有限公司研发的新一代的互联网验证码,使用点击的形式完成验证,采用专利的印刷算法以及加密算法,保证每次请求到的验证图具有极高的安全性,常见的点触验证码如下:


01

【14.2】点触验证码攻克思路

点触验证码相对其他类型验证码比较复杂,如果依靠 OCR 图像识别点触验证码,则识别难度非常大,此时就要用到互联网的验证码服务平台,这些服务平台全部都是人工在线识别,准确率非常高,原理就是先将验证码图片提交给平台,平台会返回识别结果在图片中的坐标位置,然后我们再解析坐标模拟点击即可,常见的打码平台有超级鹰、云打码等,打码平台是收费的,拿超级鹰来说,1元 = 1000题分,识别一次验证码将花费一定的题分,不同类型验证码需要的题分不同,验证码越复杂所需题分越高,比如 7 位中文汉字需要 70 题分,常见 4 ~ 6 位英文数字只要 10 题分,其他打码平台价格也都差不多

以下以超级鹰打码平台中国铁路12306官网来做练习


【14.3】模拟登录 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
2
3
4
5
6
7
8
9
10
def __init__(): 初始化 WebDriver、Chaojiying 对象等
def crack(): 破解入口、获取、识别验证码、模拟登录
def open(): 账号密码输入
def get_screenshot(): 整个页面截图
def get_touclick_element(): 获取验证码位置
def get_position(): 获取验证码坐标
def get_touclick_image(): 剪裁验证码部分
def get_points(self, captcha_result): 分析超级鹰返回的坐标
def touch_click_words(self, locations): 模拟点击符合要求的图片
def login(self): 点击登陆按钮,完成模拟登录

整个程序用到的库:

1
2
3
4
5
6
7
8
9
10
11
import time
from io import BytesIO
from PIL import Image
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from chaojiying import Chaojiying
from selenium.common.exceptions import TimeoutException

【14.4】主函数

1
2
3
if __name__ == '__main__':
crack = CrackTouClick()
crack.crack()

【14.5】初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
USERNAME = '155********'
PASSWORD = '***********'

CHAOJIYING_USERNAME = '*******'
CHAOJIYING_PASSWORD = '*******'
CHAOJIYING_SOFT_ID = '********'
CHAOJIYING_KIND = '9004'


class CrackTouClick():
def __init__(self):
self.url = 'https://kyfw.12306.cn/otn/resources/login.html'
path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe'
chrome_options = Options()
chrome_options.add_argument('--start-maximized')
self.browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
self.wait = WebDriverWait(self.browser, 20)
self.email = USERNAME
self.password = PASSWORD
self.chaojiying = Chaojiying_Client(CHAOJIYING_USERNAME, CHAOJIYING_PASSWORD, CHAOJIYING_SOFT_ID)

定义 12306 账号(USERNAME)、密码(PASSWORD)、超级鹰用户名(CHAOJIYING_USERNAME)、超级鹰登录密码(CHAOJIYING_PASSWORD)、超级鹰软件 ID(CHAOJIYING_SOFT_ID)、验证码类型(CHAOJIYING_KIND),登录链接 url:https://kyfw.12306.cn/otn/resources/login.html ,谷歌浏览器驱动的目录(path),浏览器启动参数,并将相关参数传递给超级鹰 API


【14.6】破解入口函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def crack(self):
self.open()
image = self.get_touclick_image()
bytes_array = BytesIO()
image.save(bytes_array, format='PNG')
result = self.chaojiying.PostPic(bytes_array.getvalue(), CHAOJIYING_KIND)
print(result)
locations = self.get_points(result)
self.touch_click_words(locations)
self.login()
try:
success = self.wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, '.welcome-name'), '用户姓名'))
print(success)
cc = self.browser.find_element(By.CSS_SELECTOR, '.welcome-name')
print(cc.text)
except TimeoutException:
self.chaojiying.ReportError(result['pic_id'])
self.crack()

调用 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 语句判断是否出现了用户信息,判断依据是是否有用户姓名的出现,出现的姓名和实际姓名一致则登录成功,如果失败了就重试,超级鹰会返回该分值


【14.7】账号密码输入函数

1
2
3
4
5
6
7
8
9
def open(self):
self.browser.get(self.url)
login = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.login-hd-account')))
login.click()
time.sleep(3)
username = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'input#J-userName')))
password = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'input#J-password')))
username.send_keys(self.email)
password.send_keys(self.password)

分析页面可知,登陆页面 URL 为:https://kyfw.12306.cn/otn/resources/login.html ,该页面默认出现的是扫描二维码登陆,所以要先点击账号登录,找到该 CSS 元素为 login-hd-account,调用 click() 方法实现模拟点击,此时出现账号密码输入框,同样找到其 ID 分别为 J-userNameJ-password,调用 send_keys() 方法输入账号密码


【14.8】页面截图函数

1
2
3
4
def get_screenshot(self):
screenshot = self.browser.get_screenshot_as_png()
screenshot = Image.open(BytesIO(screenshot))
return screenshot

对整个页面进行截图


【14.9】验证码元素查找函数

1
2
3
def get_touclick_element(self):
element = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.login-pwd-code')))
return element

同样分析页面,验证码所在位置的 CSS 为 login-pwd-code


【14.10】获取验证码坐标函数

1
2
3
4
5
6
7
def get_position(self):
element = self.get_touclick_element()
time.sleep(3)
location = element.location
size = element.size
top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size['width']
return (top, bottom, left, right)

location 属性可以返回该图片对象在浏览器中的位置,坐标轴是以屏幕左上角为原点,x 轴向右递增,y 轴向下递增,size 属性可以返回该图片对象的高度和宽度,由此可以得到验证码的位置信息


【14.11】验证码剪裁函数

1
2
3
4
5
6
def get_touclick_image(self, name='12306.png'):
top, bottom, left, right = self.get_position()
screenshot = self.get_screenshot()
captcha = screenshot.crop((left, top, right, bottom))
captcha.save(name)
return captcha

根据验证码的坐标信息,对页面截图进行剪裁,得到验证码部分,将其保存为 12306.png


【14.12】验证码坐标解析函数

1
2
3
4
def get_points(self, captcha_result):
groups = captcha_result.get('pic_str').split('|')
locations = [[int(number) for number in group.split(',')] for group in groups]
return locations

get_points() 方法将超级鹰的验证码识别结果变成列表的形式


【14.13】验证码模拟点击函数

1
2
3
4
def touch_click_words(self, locations):
for location in locations:
print(location)
ActionChains(self.browser).move_to_element_with_offset(self.get_touclick_element(), location[0]/1.25, location[1]/1.25).click().perform()

touch_click_words() 方法通过调用 move_to_element_with_offset() 方法依次传入解析后的坐标,点击即可


【14.14】模拟点击登陆函数

1
2
3
def login(self):
submit = self.wait.until(EC.element_to_be_clickable((By.ID, 'J-login')))
submit.click()

分析页面,找到登陆按钮的 ID 为 J-login,调用 click() 方法模拟点击按钮实现登录


【14.15】效果实现动图

最终实现效果图:(关键信息已经过打码处理)


02

【14.16】完整代码

12306.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import time
from io import BytesIO
from PIL import Image
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from chaojiying import Chaojiying_Client
from selenium.common.exceptions import TimeoutException

USERNAME = '155********'
PASSWORD = '***********'

CHAOJIYING_USERNAME = '***********'
CHAOJIYING_PASSWORD = '***********'
CHAOJIYING_SOFT_ID = '******'
CHAOJIYING_KIND = '9004'


class CrackTouClick():
def __init__(self): #登陆
self.url = 'https://kyfw.12306.cn/otn/resources/login.html'
path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe'
chrome_options = Options()
chrome_options.add_argument('--start-maximized')
self.browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
self.wait = WebDriverWait(self.browser, 20)
self.email = USERNAME
self.password = PASSWORD
self.chaojiying = Chaojiying_Client(CHAOJIYING_USERNAME, CHAOJIYING_PASSWORD, CHAOJIYING_SOFT_ID)

def crack(self):
self.open()
image = self.get_touclick_image()
bytes_array = BytesIO()
image.save(bytes_array, format='PNG')
result = self.chaojiying.PostPic(bytes_array.getvalue(), CHAOJIYING_KIND)
print(result)
locations = self.get_points(result)
self.touch_click_words(locations)
self.login()
try:
success = self.wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, '.welcome-name'), '谭仁侯'))
print(success)
cc = self.browser.find_element(By.CSS_SELECTOR, '.welcome-name')
print(cc.text)

except TimeoutException:
self.chaojiying.ReportError(result['pic_id'])
self.crack()

def open(self):
self.browser.get(self.url)
login = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.login-hd-account')))
login.click()
time.sleep(3)
username = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'input#J-userName')))
password = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'input#J-password')))
username.send_keys(self.email)
password.send_keys(self.password)

def get_screenshot(self):
screenshot = self.browser.get_screenshot_as_png()
screenshot = Image.open(BytesIO(screenshot))
return screenshot

def get_touclick_element(self):
element = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.login-pwd-code')))
return element

def get_position(self):
element = self.get_touclick_element()
time.sleep(3)
location = element.location
size = element.size
top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size['width']
return (top, bottom, left, right)

def get_touclick_image(self, name='12306.png'):
top, bottom, left, right = self.get_position()
screenshot = self.get_screenshot()
captcha = screenshot.crop((left, top, right, bottom))
captcha.save(name)
return captcha

def get_points(self, captcha_result):
groups = captcha_result.get('pic_str').split('|')
locations = [[int(number) for number in group.split(',')] for group in groups]
return locations

def touch_click_words(self, locations):
for location in locations:
print(location)
ActionChains(self.browser).move_to_element_with_offset(self.get_touclick_element(), location[0]/1.25, location[1]/1.25).click().perform()

def login(self):
submit = self.wait.until(EC.element_to_be_clickable((By.ID, 'J-login')))
submit.click()


if __name__ == '__main__':
crack = CrackTouClick()
crack.crack()

chaojiying.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import requests
from hashlib import md5


class Chaojiying_Client(object):

def __init__(self, username, password, soft_id):
self.username = username
password = password.encode('utf8')
self.password = md5(password).hexdigest()
self.soft_id = soft_id
self.base_params = {
'user': self.username,
'pass2': self.password,
'softid': self.soft_id,
}
self.headers = {
'Connection': 'Keep-Alive',
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
}

def PostPic(self, im, codetype):
"""
im: 图片字节
codetype: 题目类型 参考 http://www.chaojiying.com/price.html
"""
params = {
'codetype': codetype,
}
params.update(self.base_params)
files = {'userfile': ('ccc.jpg', im)}
r = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files, headers=self.headers)
return r.json()

def ReportError(self, im_id):
"""
im_id:报错题目的图片ID
"""
params = {
'id': im_id,
}
params.update(self.base_params)
r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
return r.json()
]]>
<blockquote> <center><font color="#1BC3FB" size="4">Python3 爬虫学习笔记第十四章 —— 【验证码对抗系列 — 点触验证码】</font></center> </blockquote>
Python3 爬虫学习笔记 C13 https://www.itrhx.com/2019/09/07/A43-Python3-spider-C13/ 2019-09-06T19:52:14.161Z 2019-09-24T12:40:56.234Z
Python3 爬虫学习笔记第十三章 —— 【验证码对抗系列 — 滑动验证码】

【13.1】关于滑动验证码

滑动验证码属于行为式验证码,需要通过用户的操作行为来完成验证,一般是根据提示用鼠标将滑块拖动到指定的位置完成验证,此类验证码背景图片采用多种图像加密技术,且添加了很多随机效果,能有效防止OCR文字识别,另外,验证码上的文字采用了随机印刷技术,能够随机采用多种字体、多种变形的实时随机印刷,防止暴力破解;斗鱼、哔哩哔哩、淘宝等平台都使用了滑动验证码


01

【13.2】滑动验证码攻克思路

利用自动化测试工具 Selenium 直接模拟人的行为方式来完成验证,首先要分析页面,想办法找到滑动验证码的完整图片、带有缺口的图片和需要滑动的图片,通过对比原始的图片和带滑块缺口的图片的像素,像素不同的地方就是缺口位置,计算出滑块缺口的位置,得到所需要滑动的距离,最后利用 Selenium 进行对滑块的拖拽,拖拽时要模仿人的行为,由于有个对准过程,所以是先快后慢,匀速移动、随机速度移动都不会成功

以下以哔哩哔哩为例来做模拟登录练习


【13.3】模拟登录 bilibili — 总体思路

首先使用 Selenium 模拟登陆 bilibili,自动输入账号密码,查找到登陆按钮并点击,使其出现滑动验证码,此时分析页面,滑动验证组件是由3个 canvas 组成,分别代表完整图片、带有缺口的图片和需要滑动的图片,3个 canvas 元素包含 CSS display 属性,display:block 为可见,display:none 为不可见,分别获取三张图片时要将其他两张图片设置为 display:none,获取元素位置后即可对图片截图并保存,通过图片像素对比,找到缺口位置即为滑块要移动的距离,随后构造滑动轨迹,按照先加速后减速的方式移动滑块完成验证。

整个程序包含的函数:

1
2
3
4
5
6
7
8
9
10
11
def init(): 初始化函数,定义全局变量
def login(): 登录函数,输入账号密码并点击登录
def find_element(): 验证码元素查找函数,查找三张图的元素
def hide_element(): 设置元素不可见函数
def show_element(): 设置元素可见函数
def save_screenshot(): 验证码截图函数,截取三张图并保存
def slide(): 滑动函数
def is_pixel_equal(): 像素判断函数,寻找缺口位置
def get_distance(): 计算滑块移动距离函数
def get_track(): 构造移动轨迹函数
def move_to_gap(): 模拟拖动函数

整个程序用到的库:

1
2
3
4
5
6
7
8
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
import time
import random

【13.4】主函数

1
2
3
4
5
if __name__ == '__main__':
init()
login()
find_element()
slide()

【13.5】初始化函数

1
2
3
4
5
6
7
8
9
10
def init():
global url, browser, username, password, wait
url = 'https://passport.bilibili.com/login'
path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe'
chrome_options = Options()
chrome_options.add_argument('--start-maximized')
browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
username = '155********'
password = '***********'
wait = WebDriverWait(browser, 20)

global 关键字定义了全局变量,随后是登录页面url、谷歌浏览器驱动的目录path、实例化 Chrome 浏览器、设置浏览器分辨率最大化、用户名、密码、WebDriverWait() 方法设置等待超时


【13.6】登录函数

1
2
3
4
5
6
7
8
9
def login():
browser.get(url)
user = wait.until(EC.presence_of_element_located((By.ID, 'login-username')))
passwd = wait.until(EC.presence_of_element_located((By.ID, 'login-passwd')))
user.send_keys(username)
passwd.send_keys(password)
login_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'a.btn.btn-login')))
time.sleep(random.random() * 3)
login_btn.click()

等待用户名输入框和密码输入框对应的 ID 节点加载出来,分析页面可知,用户名输入框 id="login-username",密码输入框 id="login-passwd",获取这两个节点,调用 send_keys() 方法输入用户名和密码,随后获取登录按钮,分析页面可知登录按钮 class="btn btn-login",随机产生一个数并将其扩大三倍作为暂停时间,最后调用 click() 方法实现登录按钮的点击


【13.7】验证码元素查找函数

1
2
3
4
5
6
7
8
9
10
11
12
13
def find_element():
c_background = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_bg.geetest_absolute')))
c_slice = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_slice.geetest_absolute')))
c_full_bg = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_fullbg.geetest_fade.geetest_absolute')))
hide_element(c_slice)
save_screenshot(c_background, 'back')
show_element(c_slice)
save_screenshot(c_slice, 'slice')
show_element(c_full_bg)
save_screenshot(c_full_bg, 'full')

我们要获取验证码的三张图片,分别是完整的图片、带有缺口的图片和需要滑动的图片,分析页面代码,这三张图片是由 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() 函数,进一步对验证码进行处理


02

【13.8】元素可见性设置函数

1
2
3
4
5
6
7
8
# 设置元素不可见
def hide_element(element):
browser.execute_script("arguments[0].style=arguments[1]", element, "display: none;")


# 设置元素可见
def show_element(element):
browser.execute_script("arguments[0].style=arguments[1]", element, "display: block;")

【13.9】验证码截图函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def save_screenshot(obj, name):
try:
pic_url = browser.save_screenshot('.\\bilibili.png')
print("%s:截图成功!" % pic_url)
left = obj.location['x']
top = obj.location['y']
right = left + obj.size['width']
bottom = top + obj.size['height']
print('图:' + name)
print('Left %s' % left)
print('Top %s' % top)
print('Right %s' % right)
print('Bottom %s' % bottom)
print('')
im = Image.open('.\\bilibili.png')
im = im.crop((left, top, right, bottom))
file_name = 'bili_' + name + '.png'
im.save(file_name)
except BaseException as msg:
print("%s:截图失败!" % msg)

location 属性可以返回该图片对象在浏览器中的位置,坐标轴是以屏幕左上角为原点,x轴向右递增,y轴向下递增,size 属性可以返回该图片对象的高度和宽度,由此可以得到验证码的位置信息,首先调用 save_screenshot() 属性对整个页面截图并保存,然后向 crop() 方法传入验证码的位置信息,由位置信息再对验证码进行剪裁并保存


【13.10】滑动函数

1
2
3
4
5
6
def slide():
distance = get_distance(Image.open('.\\bili_back.png'), Image.open('.\\bili_full.png'))
print('计算偏移量为:%s Px' % distance)
trace = get_trace(distance - 5)
move_to_gap(trace)
time.sleep(3)

get_distance() 函数传入完整的图片和缺口图片,计算滑块需要滑动的距离,再把距离信息传入 get_trace() 函数,构造滑块的移动轨迹,最后根据轨迹信息调用 move_to_gap() 函数移动滑块完成验证


【13.11】计算滑块移动距离函数

1
2
3
4
5
6
def get_distance(bg_image, fullbg_image):
distance = 60
for i in range(distance, fullbg_image.size[0]):
for j in range(fullbg_image.size[1]):
if not is_pixel_equal(fullbg_image, bg_image, i, j):
return i

get_distance() 方法即获取缺口位置的方法,此方法的参数是两张图片,一张为完整的图片,另一张为带缺口的图片,distance 为滑块的初始位置,遍历两张图片的每个像素,利用 is_pixel_equal() 像素判断函数判断两张图片同一位置的像素是否相同,比较两张图 RGB 的绝对值是否均小于定义的阈值 threshold,如果绝对值均在阈值之内,则代表像素点相同,继续遍历,否则代表不相同的像素点,即缺口的位置


【13.12】像素判断函数

1
2
3
4
5
6
7
8
9
def is_pixel_equal(bg_image, fullbg_image, x, y):
bg_pixel = bg_image.load()[x, y]
fullbg_pixel = fullbg_image.load()[x, y]
threshold = 60
if (abs(bg_pixel[0] - fullbg_pixel[0] < threshold) and abs(bg_pixel[1] - fullbg_pixel[1] < threshold) and abs(
bg_pixel[2] - fullbg_pixel[2] < threshold)):
return True
else:
return False

将完整图片和缺口图片两个对象分别赋值给变量 bg_image和 fullbg_image,接下来对比图片获取缺口。我们在这里遍历图片的每个坐标点,获取两张图片对应像素点的 RGB 数据,判断像素的各个颜色之差,abs() 用于取绝对值,如果二者的 RGB 数据差距在一定范围内,那就代表两个像素相同,继续比对下一个像素点,如果差距超过一定范围,则代表像素点不同,当前位置即为缺口位置


【13.13】构造移动轨迹函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_trace(distance):
trace = []
faster_distance = distance * (4 / 5)
start, v0, t = 0, 0, 0.1
while start < distance:
if start < faster_distance:
a = 20
else:
a = -20
move = v0 * t + 1 / 2 * a * t * t
v = v0 + a * t
v0 = v
start += move
trace.append(round(move))
return trace

get_trace() 方法传入的参数为移动的总距离,返回的是运动轨迹,运动轨迹用 trace 表示,它是一个列表,列表的每个元素代表每次移动多少距离,利用 Selenium 进行对滑块的拖拽时要模仿人的行为,由于有个对准过程,所以是先快后慢,匀速移动、随机速度移动都不会成功,因此要设置一个加速和减速的距离,这里设置加速距离 faster_distance 是总距离 distance 的4/5倍,滑块滑动的加速度用 a 来表示,当前速度用 v 表示,初速度用 v0 表示,位移用 move 表示,所需时间用 t 表示,它们之间满足以下关系:

1
2
move = v0 * t + 0.5 * a * t * t 
v = v0 + a * t

设置初始位置、初始速度、时间间隔分别为0, 0, 0.1,加速阶段和减速阶段的加速度分别设置为20和-20,直到运动轨迹达到总距离时,循环终止,最后得到的 trace 记录了每个时间间隔移动了多少位移,这样滑块的运动轨迹就得到了


【13.14】模拟拖动函数

1
2
3
4
5
6
7
def move_to_gap(trace):
slider = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div.geetest_slider_button')))
ActionChains(browser).click_and_hold(slider).perform()
for x in trace:
ActionChains(browser).move_by_offset(xoffset=x, yoffset=0).perform()
time.sleep(0.5)
ActionChains(browser).release().perform()

传入的参数为运动轨迹,首先查找到滑动按钮,然后调用 ActionChains 的 click_and_hold() 方法按住拖动底部滑块,perform() 方法用于执行,遍历运动轨迹获取每小段位移距离,调用 move_by_offset() 方法移动此位移,最后调用 release() 方法松开鼠标即可


【13.15】效果实现动图

最终实现效果图:(关键信息已经过打码处理)


03

【13.16】完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
import time
import random
from PIL import Image


def init():
global url, browser, username, password, wait
url = 'https://passport.bilibili.com/login'
path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe'
chrome_options = Options()
chrome_options.add_argument('--start-maximized')
browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
username = '155********'
password = '***********'
wait = WebDriverWait(browser, 20)


def login():
browser.get(url)
user = wait.until(EC.presence_of_element_located((By.ID, 'login-username')))
passwd = wait.until(EC.presence_of_element_located((By.ID, 'login-passwd')))
user.send_keys(username)
passwd.send_keys(password)
login_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'a.btn.btn-login')))
time.sleep(random.random() * 3)
login_btn.click()


def find_element():
c_background = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_bg.geetest_absolute')))
c_slice = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_slice.geetest_absolute')))
c_full_bg = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_fullbg.geetest_fade.geetest_absolute')))
hide_element(c_slice)
save_screenshot(c_background, 'back')
show_element(c_slice)
save_screenshot(c_slice, 'slice')
show_element(c_full_bg)
save_screenshot(c_full_bg, 'full')


def hide_element(element):
browser.execute_script("arguments[0].style=arguments[1]", element, "display: none;")


def show_element(element):
browser.execute_script("arguments[0].style=arguments[1]", element, "display: block;")


def save_screenshot(obj, name):
try:
pic_url = browser.save_screenshot('.\\bilibili.png')
print("%s:截图成功!" % pic_url)
left = obj.location['x']
top = obj.location['y']
right = left + obj.size['width']
bottom = top + obj.size['height']
print('图:' + name)
print('Left %s' % left)
print('Top %s' % top)
print('Right %s' % right)
print('Bottom %s' % bottom)
print('')
im = Image.open('.\\bilibili.png')
im = im.crop((left, top, right, bottom))
file_name = 'bili_' + name + '.png'
im.save(file_name)
except BaseException as msg:
print("%s:截图失败!" % msg)


def slide():
distance = get_distance(Image.open('.\\bili_back.png'), Image.open('.\\bili_full.png'))
print('计算偏移量为:%s Px' % distance)
trace = get_trace(distance - 5)
move_to_gap(trace)
time.sleep(3)


def get_distance(bg_image, fullbg_image):
distance = 60
for i in range(distance, fullbg_image.size[0]):
for j in range(fullbg_image.size[1]):
if not is_pixel_equal(fullbg_image, bg_image, i, j):
return i


def is_pixel_equal(bg_image, fullbg_image, x, y):
bg_pixel = bg_image.load()[x, y]
fullbg_pixel = fullbg_image.load()[x, y]
threshold = 60
if (abs(bg_pixel[0] - fullbg_pixel[0] < threshold) and abs(bg_pixel[1] - fullbg_pixel[1] < threshold) and abs(
bg_pixel[2] - fullbg_pixel[2] < threshold)):
return True

else:
return False


def get_trace(distance):
trace = []
faster_distance = distance * (4 / 5)
start, v0, t = 0, 0, 0.1
while start < distance:
if start < faster_distance:
a = 20
else:
a = -20
move = v0 * t + 1 / 2 * a * t * t
v = v0 + a * t
v0 = v
start += move
trace.append(round(move))
return trace


def move_to_gap(trace):
slider = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div.geetest_slider_button')))
ActionChains(browser).click_and_hold(slider).perform()
for x in trace:
ActionChains(browser).move_by_offset(xoffset=x, yoffset=0).perform()
time.sleep(0.5)
ActionChains(browser).release().perform()


if __name__ == '__main__':
init()
login()
find_element()
slide()
]]>
<blockquote> <center><font color="#1BC3FB" size="4">Python3 爬虫学习笔记第十三章 —— 【验证码对抗系列 — 滑动验证码】</font></center> </blockquote>
Python3 爬虫学习笔记 C12 https://www.itrhx.com/2019/09/05/A42-Python3-spider-C12/ 2019-09-05T14:54:48.887Z 2019-09-24T12:40:54.127Z
Python3 爬虫学习笔记第十二章 —— 【验证码对抗系列 — 图形验证码】

【12.1】关于普通图形验证码

普通图形验证码一般由四位纯数字、纯字母或者字母数字组合构成,是最常见的验证码,也是最简单的验证码,利用 tesserocr 或者 pytesseract 库即可识别此类验证码,前提是已经安装好 Tesseract-OCR 软件


01

【12.2】tesserocr 库识别验证码

简单示例:

1
2
3
4
5
6
import tesserocr
from PIL import Image

image = Image.open('code.png')
result = tesserocr.image_to_text(image)
print(result)

新建一个 Image 对象,调用 tesserocr 的 image_to_text() 方法,传入 Image 对象即可完成识别,另一种方法:

1
2
import tesserocr
print(tesserocr.file_to_text('code.png'))

【12.3】pytesseract 库识别验证码

简单示例:

1
2
3
4
5
6
7
import pytesseract
from PIL import Image

img = Image.open('code.png')
img = img.convert('RGB')
img.show()
print(pytesseract.image_to_string(img))

pytesseract 的各种方法:

  • get_tesseract_version:返回 Tesseract 的版本信息;
  • image_to_string:将图像上的 Tesseract OCR 运行结果返回到字符串;
  • image_to_boxes:返回包含已识别字符及其框边界的结果;
  • image_to_data:返回包含框边界,置信度和其他信息的结果。需要 Tesseract 3.05+;
  • image_to_osd:返回包含有关方向和脚本检测的信息的结果。

有关参数:

image_to_data(image, lang='', config='', nice=0, output_type=Output.STRING)

  • image:图像对象;
  • lang:Tesseract 语言代码字符串;
  • config:任何其他配置为字符串,例如:config=’–psm 6’;
  • nice:修改 Tesseract 运行的处理器优先级。Windows不支持。尼斯调整了类似 unix 的流程的优点;
  • output_type:类属性,指定输出的类型,默认为string。

lang 参数,常见语言代码如下:

  • chi_sim:简体中文
  • chi_tra:繁体中文
  • eng:英文
  • rus:俄罗斯语
  • fra:法语
  • deu:德语
  • jpn:日语

【12.4】验证码处理

利用 Image 对象的 convert() 方法传入不同参数可以对验证码做一些额外的处理,如转灰度、二值化等操作,经过处理过后的验证码会更加容易被识别,识别准确度更高,各种参数及含义:

  • 1:1位像素,黑白,每字节一个像素存储;
  • L:8位像素,黑白;
  • P:8位像素,使用调色板映射到任何其他模式;
  • RGB:3x8位像素,真彩色;
  • RGBA:4x8位像素,带透明度掩模的真彩色;
  • CMYK:4x8位像素,分色;
  • YCbCr:3x8位像素,彩色视频格式;
  • I:32位有符号整数像素;
  • F:32位浮点像素。

示例:

1
2
3
4
5
6
7
8
import pytesseract
from PIL import Image

image = Image.open('code.png')
image = image.convert('L')
image.show()
result = pytesseract.image_to_string(image)
print(result)

Image 对象的 convert() 方法参数传入 L,即可将图片转化为灰度图像,转换前后对比:


02
1
2
3
4
5
6
7
8
import pytesseract
from PIL import Image

image = Image.open('code.png')
image = image.convert('1')
image.show()
result = pytesseract.image_to_string(image)
print(result)

Image 对象的 convert() 方法参数传入 1,即可将图片进行二值化处理,处理前后对比:


03

【12.5】tesserocr 与 pytesserocr 相关资料

]]>
<blockquote> <center><font color="#1BC3FB" size="4">Python3 爬虫学习笔记第十二章 —— 【验证码对抗系列 — 图形验证码】</font></center> </blockquote>