From 066b51551ef5016f42eed169a992178af9b29d6f Mon Sep 17 00:00:00 2001 From: TRHX Date: Mon, 21 Oct 2019 21:40:25 +0800 Subject: [PATCH] Back up my www.itrhx.com blog --- source/_posts/A39-Python3-spider-C09.md | 26 +- ...maoyan.md => A51-pyspider-maoyantop100.md} | 50 +- ...douban.md => A52-pyspider-doubantop250.md} | 43 +- source/_posts/A53-hexo-backup.md | 2 +- source/_posts/A54-pyspider-anjuke.md | 225 ++++++++ source/_posts/A55-pyspider-hupu.md | 255 +++++++++ source/_posts/A56-pyspider-bilibili-login.md | 536 ++++++++++++++++++ source/_posts/A57-pyspider-12306-login.md | 444 +++++++++++++++ source/_posts/A58-pyspider-58tongcheng.md | 521 +++++++++++++++++ source/comments/index.md | 3 +- source/friends/index.md | 98 +++- themes/material-x-1.2.1/_config.yml | 26 +- .../layout/_partial/article.ejs | 4 +- .../layout/_partial/footer.ejs | 2 +- .../material-x-1.2.1/layout/_partial/post.ejs | 2 +- .../source/less/_article.less | 5 +- .../material-x-1.2.1/source/less/_base.less | 9 +- .../material-x-1.2.1/source/less/_color.less | 4 +- .../material-x-1.2.1/source/less/_fonts.less | 7 +- .../material-x-1.2.1/source/less/_main.less | 2 +- 20 files changed, 2182 insertions(+), 82 deletions(-) rename source/_posts/{A51-pyspider-combat-maoyan.md => A51-pyspider-maoyantop100.md} (74%) rename source/_posts/{A52-pyspider-combat-douban.md => A52-pyspider-doubantop250.md} (88%) create mode 100644 source/_posts/A54-pyspider-anjuke.md create mode 100644 source/_posts/A55-pyspider-hupu.md create mode 100644 source/_posts/A56-pyspider-bilibili-login.md create mode 100644 source/_posts/A57-pyspider-12306-login.md create mode 100644 source/_posts/A58-pyspider-58tongcheng.md diff --git a/source/_posts/A39-Python3-spider-C09.md b/source/_posts/A39-Python3-spider-C09.md index 68117c3e4..53b80d0b0 100644 --- a/source/_posts/A39-Python3-spider-C09.md +++ b/source/_posts/A39-Python3-spider-C09.md @@ -243,7 +243,29 @@ id,name,age 10003,Jordan,21 ``` -列与列之间的分隔符是可以修改的,只需要传入 delimiter 参数即可: +默认每一行之间是有一行空格的,可以使用参数 `newline` 来去除空行: + +```python +import csv + +with open('data.csv', 'w', newline='') as csvfile: + writer = csv.writer(csvfile) + writer.writerow(['id', 'name', 'age']) + writer.writerow(['10001', 'TRHX', 20]) + writer.writerow(['10002', 'Bob', 22]) + writer.writerow(['10003', 'Jordan', 21]) +``` + +输出结果: + +```python +id,name,age +10001,TRHX,20 +10002,Bob,22 +10003,Jordan,21 +``` + +列与列之间的分隔符是可以修改的,只需要传入 `delimiter` 参数即可: ```python import csv @@ -268,7 +290,7 @@ id name age 10003 Jordan 21 ``` -调用 writerows 方法也可以同时写入多行,此时参数就需要为二维列表: +调用 `writerows` 方法也可以同时写入多行,此时参数就需要为二维列表: ```python import csv diff --git a/source/_posts/A51-pyspider-combat-maoyan.md b/source/_posts/A51-pyspider-maoyantop100.md similarity index 74% rename from source/_posts/A51-pyspider-combat-maoyan.md rename to source/_posts/A51-pyspider-maoyantop100.md index 892cd923a..1a890777d 100644 --- a/source/_posts/A51-pyspider-combat-maoyan.md +++ b/source/_posts/A51-pyspider-maoyantop100.md @@ -10,21 +10,31 @@ thumbnail: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/thumbnail/com avatar: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/images/trhx.png --- -爬取时间:2019-09-24 -爬取难度:★☆☆☆☆☆ -请求链接:[猫眼电影TOP100榜](https://maoyan.com/board/4) -爬取目标:猫眼 TOP100 的电影名称、排名、主演、上映时间、评分、封面图地址,数据保存为 CSV 文件 -涉及知识:请求库 requests、解析库 lxml、Xpath 语法、CSV 文件储存 +> 爬取时间: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,实现循环爬取每一页 ```python @@ -38,6 +48,10 @@ if __name__ == '__main__': index = index_page(i) ``` +--- + +# 【2x00】解析模块 + 定义一个页面解析函数 `parse_page()`,使用 lxml 解析库的 Xpath 方法依次提取电影排名(ranking)、电影名称(movie_name)、主演(performer)、上映时间(releasetime)、评分(score)、电影封面图 url(movie_img) 通过对主演部分的提取发现有多余的空格符和换行符,循环 performer 列表,使用 `strip()` 方法去除字符串头尾空格和换行符 @@ -67,7 +81,11 @@ def parse_page(content): return zip(ranking, movie_name, performer, releasetime, score, movie_img) ``` -最后定义一个 `save_results()` 函数,将所有数据保存到 `maoyan.csv` 文件 +--- + +# 【3x00】数据储存模块 + +定义一个 `save_results()` 函数,将所有数据保存到 `maoyan.csv` 文件 ```python def save_results(result): @@ -76,9 +94,21 @@ def save_results(result): writer.writerow(result) ``` -完整代码: +--- + +# 【4x00】完整代码 ```python +# ============================================= +# --*-- 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 @@ -127,11 +157,13 @@ if __name__ == '__main__': results = parse_page(index) for i in results: save_results(i) - print('数据爬取完毕!!') + print('数据爬取完毕!') ``` -爬取到的数据(maoyan.csv 文件): +--- + +# 【4x00】数据截图 ![01](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A51/01.png) diff --git a/source/_posts/A52-pyspider-combat-douban.md b/source/_posts/A52-pyspider-doubantop250.md similarity index 88% rename from source/_posts/A52-pyspider-combat-douban.md rename to source/_posts/A52-pyspider-doubantop250.md index fb9d65f80..efdcdbcf2 100644 --- a/source/_posts/A52-pyspider-combat-douban.md +++ b/source/_posts/A52-pyspider-doubantop250.md @@ -10,24 +10,29 @@ thumbnail: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/thumbnail/com avatar: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/images/trhx.png --- -爬取时间:2019-09-27 -爬取难度:★★☆☆☆☆ -请求链接:[豆瓣电影 Top 250](https://movie.douban.com/top250) 以及每部电影详情页 -爬取目标:爬取榜单上每一部电影详情页的数据,保存为 CSV 文件;下载所有电影海报到本地 -涉及知识:请求库 requests、解析库 lxml、Xpath 语法、正则表达式、CSV 和二进制数据储存、列表操作 +> 爬取时间: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】循环爬取首页 +# 【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_urls`,`m_urls` 是一个列表,列表元素就是电影详情页的 URL @@ -203,15 +208,19 @@ with open(poster_path, "wb")as f: --- -# 【5x00】程序不足的地方 - -程序不足的地方:豆瓣电影有反爬机制,当程序爬取到大约 150 条数据的时候,IP 就会被封掉,第二天 IP 才会解封,可以考虑综合使用多个代理、多个 User-Agent、随机时间暂停等方法进行爬取 - ---- - -# 【6x00】完整代码 +# 【5x00】完整代码 ```python +# ============================================= +# --*-- 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 @@ -331,7 +340,7 @@ if __name__ == '__main__': --- -# 【7x00】数据截图 +# 【6x00】数据截图 ![03](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A52/03.png) @@ -340,3 +349,9 @@ if __name__ == '__main__': ![04](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A52/04.png) + +--- + +# 【7x00】程序不足的地方 + +程序不足的地方:豆瓣电影有反爬机制,当程序爬取到大约 150 条数据的时候,IP 就会被封掉,第二天 IP 才会解封,可以考虑综合使用多个代理、多个 User-Agent、随机时间暂停等方法进行爬取 diff --git a/source/_posts/A53-hexo-backup.md b/source/_posts/A53-hexo-backup.md index 4a5a545e3..bf3ba4306 100644 --- a/source/_posts/A53-hexo-backup.md +++ b/source/_posts/A53-hexo-backup.md @@ -60,7 +60,7 @@ $ hexo backup 或者使用以下简写命令也可以: ```bash -$ hexo d +$ hexo b ``` 备份成功后可以在你的仓库分支下看到备份的原始文件: diff --git a/source/_posts/A54-pyspider-anjuke.md b/source/_posts/A54-pyspider-anjuke.md new file mode 100644 index 000000000..938c25aec --- /dev/null +++ b/source/_posts/A54-pyspider-anjuke.md @@ -0,0 +1,225 @@ +--- +title: Python3 爬虫实战 — 安居客武汉二手房 +tags: + - 爬虫 + - 安居客 +categories: + - Python3 学习笔记 + - 爬虫实战 +thumbnail: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/thumbnail/combat.png +avatar: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/images/trhx.png +--- + +> 爬取时间: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】页面整体分析 + +分析 [安居客武汉二手房页面](https://wuhan.anjuke.com/sale/),这次爬取实战准备使用 BeautifulSoup 解析库,熟练 BeautifulSoup 解析库的用法,注意到该页面与其他页面不同的是,不能一次性看到到底有多少页,以前知道一共有多少页,直接一个循环爬取就行了,虽然可以通过改变 url 来尝试找到最后一页,但是这样就显得不程序员了😂,因此可以通过 BeautifulSoup 解析 `下一页按钮`,提取到下一页的 url,直到没有 `下一页按钮` 这个元素为止,从而实现所有页面的爬取,剩下的信息提取和储存就比较简单了 + +--- + +# 【2x00】解析模块 + +分析页面,可以发现每条二手房信息都是包含在 `
  • ` 标签内的,因此可以使用 BeautifulSoup 解析页面得到所有的 `
  • ` 标签,然后再循环访问每个 `
  • ` 标签,依次解析得到每条二手房的各种信息 + + +![01](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A54/01.png) + + +```python +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页就行了,但是如果有一天页面数增加了呢,那么代码的可维护性就不好了,我们可以观察 `下一页按钮` ,当存在下一页的时候,是 `` 标签,并且带有下一页的 URL,不存在下一页的时候是 `` 标签,因此可以写个 `if` 语句,判断是否存在此 `` 标签,若存在,表示有下一页,然后提取其 `href` 属性并传给解析模块,实现后面所有页面的信息提取,此外,由于安居客有反爬系统,我们还可以利用 Python中的 `random.randint()` 方法,在两个数值之间随机取一个数,传入 `time.sleep()` 方法,实现随机暂停爬取 + + +![02](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A54/02.png) + + +```python +# 判断是否还有下一页 +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 文件中即可 + +```python +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】完整代码 + +```python +# ============================================= +# --*-- 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](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A54/03.png) + + +--- +# 【7x00】程序不足的地方 + +- 虽然使用了随机暂停爬取的方法,但是在爬取了大约 20 页的数据后依然会出现验证页面,导致程序终止 + +- 原来设想的是可以由用户手动输入城市的拼音来查询不同城市的信息,方法是把用户输入的城市拼音和其他参数一起构造成一个 URL,然后对该 URL 发送请求,判断请求返回的代码,如果是 200 就代表可以访问,也就是用户输入的城市是正确的,然而发现即便是输入错误,该 URL 依然可以访问,只不过会跳转到一个正确的页面,没有搞清楚是什么原理,也就无法实现由用户输入城市来查询这个功能 diff --git a/source/_posts/A55-pyspider-hupu.md b/source/_posts/A55-pyspider-hupu.md new file mode 100644 index 000000000..77abdb343 --- /dev/null +++ b/source/_posts/A55-pyspider-hupu.md @@ -0,0 +1,255 @@ +--- +title: Python3 爬虫实战 — 虎扑论坛步行街 +tags: + - 爬虫 + - 虎扑论坛 +categories: + - Python3 学习笔记 + - 爬虫实战 +thumbnail: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/thumbnail/combat.png +avatar: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/images/trhx.png +--- + +> 爬取时间: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 页的数据 + +```python +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()` 方法,再将每条帖子的列表依次加到另一个新列表里,最终返回的是类似于如下形式的列表: + +```python +[['帖子1', '作者1'], ['帖子2', '作者2'], ['帖子3', '作者3']] +``` + +这样做的目的是:方便 MongoDB 依次储存每一条帖子的信息 + +```python +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` + +将解析函数返回的列表传入到储存函数,依次循环该列表,对每一条帖子的信息进行提取并储存 + +```python +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】完整代码 + +```python +# ============================================= +# --*-- 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](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A55/01.png) + + +# 【6x00】程序不足的地方 + +程序只能爬取前 10 页的数据,因为虎扑论坛要求从第 11 页开始,必须登录账号才能查看,并且登录时会有智能验证,可以使用自动化测试工具 Selenium 模拟登录账号后再进行爬取。 diff --git a/source/_posts/A56-pyspider-bilibili-login.md b/source/_posts/A56-pyspider-bilibili-login.md new file mode 100644 index 000000000..25ec80aea --- /dev/null +++ b/source/_posts/A56-pyspider-bilibili-login.md @@ -0,0 +1,536 @@ +--- +title: Python3 爬虫实战 — 模拟登陆哔哩哔哩【滑动验证码对抗】 +tags: + - 爬虫 + - 哔哩哔哩 +categories: + - Python3 学习笔记 + - 爬虫实战 +thumbnail: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/thumbnail/combat.png +avatar: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/images/trhx.png +--- + +> 登陆时间: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](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A56/01.png) + + +- 利用自动化测试工具 Selenium 直接模拟人的行为方式来完成验证 + +- 分析页面,想办法找到滑动验证码的完整图片、带有缺口的图片和需要滑动的图片 + +- 对比原始的图片和带缺口的图片的像素,像素不同的地方就是缺口位置 + +- 计算出滑块缺口的位置,得到所需要滑动的距离 + +- 拖拽时要模仿人的行为,由于有个对准过程,所以要构造先快后慢的运动轨迹 + +- 最后利用 Selenium 进行对滑块的拖拽 + + +![02](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A56/02.png) + + +--- + +# 【2x00】登陆模块 + +## 【2x01】初始化函数 + +```python +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】登陆函数 + +```python +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】验证码元素查找函数 + +```python +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](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A56/03.png) + + +--- + +## 【3x02】元素可见性设置函数 + +```python +# 设置元素不可见 +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】验证码截图函数 + +```python +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】滑动主函数 + +```python +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】缺口位置寻找函数 + +```python +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_image` 和 `fullbg_image`,接下来对比图片获取缺口。遍历图片的每个坐标点,获取两张图片对应像素点的 RGB 数据,判断像素的各个颜色之差,`abs()` 用于取绝对值,比较两张图 RGB 的绝对值是否均小于定义的阈值 threshold,如果绝对值均在阈值之内,则代表像素点相同,继续遍历,否则代表不相同的像素点,即缺口的位置 + +--- + +## 【4x03】计算滑块移动距离函数 + + +```python +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】构造移动轨迹函数 + +```python +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 表示,它们之间满足以下关系: + +```python +move = v0 * t + 0.5 * a * t * t +v = v0 + a * t +``` + +设置初始位置、初始速度、时间间隔分别为0, 0, 0.1,加速阶段和减速阶段的加速度分别设置为10和-10,直到运动轨迹达到总距离时,循环终止,最后得到的 trace 记录了每个时间间隔移动了多少位移,这样滑块的运动轨迹就得到了 + +--- + +## 【4x05】模拟拖动函数 + +```python +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】完整代码 + +```python +# ============================================= +# --*-- 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](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A56/bilibili.gif) + diff --git a/source/_posts/A57-pyspider-12306-login.md b/source/_posts/A57-pyspider-12306-login.md new file mode 100644 index 000000000..db2334d47 --- /dev/null +++ b/source/_posts/A57-pyspider-12306-login.md @@ -0,0 +1,444 @@ +--- +title: Python3 爬虫实战 — 模拟登陆12306【点触验证码对抗】 +tags: + - 爬虫 + - 12306 +categories: + - Python3 学习笔记 + - 爬虫实战 +thumbnail: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/thumbnail/combat.png +avatar: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/images/trhx.png +--- + +> 登陆时间: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](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A57/01.png) + + +- 利用自动化测试工具 Selenium 直接模拟人的行为方式来完成验证 + +- 发送请求,出现验证码后,剪裁并保存验证码图片 + +- 选择在线打码平台,获取其API,以字节流格式发送图片 + +- 打码平台人工识别验证码,返回验证码的坐标信息 + +- 解析返回的坐标信息,模拟点击验证码,完成验证后点击登陆 + + +![02](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A57/02.png) + + +--- + +# 【2x00】打码平台选择 + +关于打码平台:在线打码平台全部都是人工在线识别,准确率非常高,原理就是先将验证码图片提交给平台,平台会返回识别结果在图片中的坐标位置,然后我们再解析坐标模拟点击即可,常见的打码平台有超级鹰、云打码等,打码平台是收费的,拿超级鹰来说,1元 = 1000题分,识别一次验证码将花费一定的题分,不同类型验证码需要的题分不同,验证码越复杂所需题分越高,比如 7 位中文汉字需要 70 题分,常见 4 ~ 6 位英文数字只要 10 题分,其他打码平台价格也都差不多,本次实战使用[超级鹰打码平台](http://www.chaojiying.com/) + +使用打码平台:在超级鹰打码平台注册账号,官网:http://www.chaojiying.com/ ,充值一块钱得到 1000 题分,在用户中心里面申请一个软件 ID ,在[价格体系](http://www.chaojiying.com/price.html)里面确定验证码的类型,先观察 12306 官网,发现验证码是要我们点击所有满足条件的图片,一般有 1 至 4 张图片满足要求,由此可确定在超级鹰打码平台的验证码类型为 9004(坐标多选,返回1~4个坐标,如:x1,y1|x2,y2|x3,y3), 然后在[开发文档](http://www.chaojiying.com/api.html)里面获取其 [Python API](http://www.chaojiying.com/download/Chaojiying_Python.rar),下载下来以备后用 + +--- + +# 【3x00】初始化模块 + +## 【3x01】初始化函数 + +```python +# 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】账号密码输入函数 + +```python +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-userName` 和 `J-password`,调用 `send_keys()` 方法输入账号密码 + +--- + +# 【4x00】验证码处理模块 + +```python +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,如果识别成功,典型的返回结果类似于: + +```python +{'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】验证码图片剪裁函数 + +```python +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】验证码坐标解析函数 + +```python +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】模拟点击验证码函数 + +```python +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】登录模块 + +```python +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 + +```python +# ============================================= +# --*-- 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 + +```python +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](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A57/12306.gif) + diff --git a/source/_posts/A58-pyspider-58tongcheng.md b/source/_posts/A58-pyspider-58tongcheng.md new file mode 100644 index 000000000..08f35cc9a --- /dev/null +++ b/source/_posts/A58-pyspider-58tongcheng.md @@ -0,0 +1,521 @@ +--- +title: Python3 爬虫实战 — 58同城武汉出租房【加密字体对抗】 +tags: + - 爬虫 + - 58同城 +categories: + - Python3 学习笔记 + - 爬虫实战 +thumbnail: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/thumbnail/combat.png +avatar: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/images/trhx.png +--- + +> 爬取时间: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](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A58/01.png) + + +我们通过控制台看到的乱码事实上是由于 unicode 编码导致,查看网页源代码,我们才能看到他真正的编码信息 + + +![02](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A58/02.png) + + +要攻克加密字体,那么我们肯定要分析他的字体文件了,先想办法得到他的加密字体文件,同样查看源代码,在源代码中搜索 `fangchan-secret` 的字体信息 + + +![03](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A58/03.png) + + +选中的蓝色部分就是 base64 编码的加密字体字符串了,我们将其解码成二进制编码,写进 `.woff` 的字体文件,这个过程可以通过以下代码实现: + +```python +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](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A58/04.png) + + +观察我们在网页源代码中看到的编码:类似于 `龤`、`龒` + +对比字体文件对应的编码:类似于 `uni9FA4`、`nui9F92` + +可以看到除了前面三个字符不一样以外,后面的字符都是一样的,只不过英文大小写有所差异 + +现在我们可能会想到,直接把编码替换成对应的数字不就OK了?然而并没有这么简单 + +尝试刷新一下网页,可以观察到 base64 编码的加密字体字符串会改变,也就是说编码和数字并不是一一对应的,再次获取几个字体文件,通过对比就可以看出来 + + +![05](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A58/05.png) + + + +可以看到,虽然每次数字对应的编码都不一样,但是编码总是这10个,是不变的,那么编码与数字之间肯定存在某种对应关系,,我们可以将字体文件转换为 xml 文件来观察其中的对应关系,改进原来的代码即可实现转换功能: + +```python +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` 文件并分析,在 `` 标签内可以看到熟悉的类似于 `0x9476`、`0x958f` 的编码,其后四位字符恰好是网页字体的加密编码,可以看到每一个编码后面都对应了一个 `glyph` 开头的编码 + +将其与 `58font.woff` 文件对比,可以看到 code 为 `0x958f` 这个编码对应的是数字 `3`,对应的 name 编码是 `glyph00004` + + +![06](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A58/06.png) + + +我们再次获取一个字体文件作为对比分析 + + +![07](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A58/07.png) + + +依然是 `0x958f` 这个编码,两次对应的 name 分别是 `glyph00004` 和 `glyph00007`,两次对应的数字分别是 `3` 和 `6`,那么结论就来了,每次发送请求,code 对应的 name 会随机发生变化,而 name 对应的数字不会发生变化,`glyph00001` 对应数字 `0`、`glyph00002` 对应数字 `1`,以此类推 + +那么以 `glyph` 开头的编码是如何对应相应的数字的呢?在 xml 文件里面,每个编码都有一个 `TTGlyph` 的标签,标签里面是一行一行的类似于 x,y 坐标的东西,这个其实就是用来绘制字体的,用 matplotlib 根据坐标画个图,就可以看到是一个数字 + + +![08](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A58/08.png) + + +此时,我们就知道了编码与数字的对应关系,下一步,我们可以查找 xml 文件里,编码对应的 name 的值,也就是以 `glyph` 开头的编码,然后返回其对应的数字,再替换掉网页源代码里的编码,就能成功获取到我们需要的信息了! + +总结一下攻克加密字体的大致思路: + +- 分析网页,找到对应的加密字体文件 + +- 如果引用的加密字体是一个 base64 编码的字符串,则需要转换成二进制并保存到 woff 字体文件中 + +- 将字体文件转换成 xml 文件 + +- 用 FontCreator 软件观察字体文件,结合 xml 文件,分析其编码与真实字体的关系 + +- 搞清楚编码与字体的关系后,想办法将编码替换成正常字体 + +--- + +# 【2x00】思维导图 + + +![09](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A58/09.png) + + +--- + +# 【3x00】加密字体处理模块 + +## 【3x01】获取字体文件并转换为xml文件 + +```python +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】将加密字体编码与真实字体进行匹配 + +```python +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` 对应数字 `0`、`glyph00002` 对应数字 `1`,以此类推,所以可以将其构造成为一个字典 `glyph_list` + +同样将十个 code(即类似于 `0x9476` 的加密字体编码)构造成一个列表 + +循环查找这十个 `code` 在 xml 文件里对应的 `name` 的值,然后将 `name` 的值与字典文件的 `key` 值进行对比,如果两者值相同,则获取这个 `key` 的 `value` 值,最终得到的列表 `num_list`,里面的元素就是 `unicode_list` 列表里面每个加密字体的真实值 + +--- + +## 【3x03】替换掉网页中所有的加密字体编码 + +```python +def replace_font(num, page_response): + # 9476 958F 993C 9A4B 9E3A 9EA3 9F64 9F92 9FA4 9FA5 + result = page_response.replace('鑶', num[0]).replace('閏', num[1]).replace('餼', num[2]).replace('驋', num[3]).replace('鸺', num[4]).replace('麣', num[5]).replace('齤', num[6]).replace('龒', num[7]).replace('龤', num[8]).replace('龥', num[9]) + print('已成功将所有加密字体替换!') + return result + +``` + +传入由上一步 `find_font()` 函数得到的真实字体的列表,利用 `replace()` 方法,依次将十个加密字体编码替换掉 + +--- + +# 【4x00】租房信息提取模块 + +```python +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](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A58/10.png) + + +--- + +# 【5x00】MySQL数据储存模块 + +## 【5x01】创建MySQL数据库的表 + +```python +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数据库 + +```python +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】完整代码 + +```python +# ============================================= +# --*-- 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('鑶', num[0]).replace('閏', num[1]).replace('餼', num[2]).replace('驋', num[3]).replace('鸺', num[4]).replace('麣', num[5]).replace('齤', num[6]).replace('龒', num[7]).replace('龤', num[8]).replace('龥', 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](https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-PIC/A58/11.png) + diff --git a/source/comments/index.md b/source/comments/index.md index 209d22916..4130e7e87 100644 --- a/source/comments/index.md +++ b/source/comments/index.md @@ -6,5 +6,4 @@ meta: footer: false sidebar: false --- -
    GITHUB       CSDN       博客园      知乎      QQ      TELEGRAM      EMAIL      RSS


    采用 Gitalk 评论系统,需使用 GitHub 账号登录,请尽情灌水吧!


    热烈庆祝中华人民共和国成立七十周年💖




    - +
                                             


    采用 Gitalk 评论系统,需使用 GitHub 账号登录,请尽情灌水吧!😉



    diff --git a/source/friends/index.md b/source/friends/index.md index 127b711fc..bbeac7192 100644 --- a/source/friends/index.md +++ b/source/friends/index.md @@ -28,21 +28,14 @@ links: - 人工智能 - 人生感悟 - - name: 欧阳博客 - avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/5658.png - url: https://5658.pw/ - backgroundColor: '#967ADC' - textColor: '#fff' - tags: - - 三分孤独,七分狂傲 - - name: shansan avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/shansan.jpeg - url: https://shansan.top/ + url: https://shan333.cn/ backgroundColor: '#A47366' textColor: '#fff' tags: - C++ + - Python - name: yue_luo‘s Blog avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/yueluo.png @@ -58,7 +51,7 @@ links: backgroundColor: '#B96948' textColor: '#fff' tags: - - 生信 + - 生物信息 - Python - name: 一去二三遥 @@ -87,14 +80,15 @@ links: textColor: '#fff' tags: - iOS + - 前端 - name: 清酒踏月 avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/lkxin.png url: https://www.lkxin.cn/ - backgroundColor: '#FE7472' + backgroundColor: '#967ADC' textColor: '#fff' tags: - - 技术 + - 网络技术 - 前端 - name: Mark's blog @@ -121,7 +115,9 @@ links: backgroundColor: '#53AECA' textColor: '#fff' tags: - - 今日事 今日畢 + - Android + - Java + - 前端 - name: attack204 avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/attack204.png @@ -148,7 +144,8 @@ links: backgroundColor: '#E9535B' textColor: '#fff' tags: - - 技术 + - Python + - JavaScript - 二次元 - name: asplun @@ -165,8 +162,8 @@ links: backgroundColor: '#8FB0C7' textColor: '#fff' tags: - - 人工智能 - - 人生感悟 + - 前端 + - 摄影 - name: Tohot avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/tohot.jpg @@ -183,7 +180,8 @@ links: backgroundColor: '#34A853' textColor: '#fff' tags: - - 心念所囚即为牢笼·心念所驻即为坚城 + - 海员 + - 技术分享 - name: 漫道求索 avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/yangyufeng96.jpg @@ -227,7 +225,7 @@ links: backgroundColor: '#696969' textColor: '#fff' tags: - - 念念不忘,必有回响 + - 机器学习 - name: Mohen's blog avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/mcmohen.jpg @@ -251,7 +249,7 @@ links: - name: Steven_MengのBlog avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/stevenmhy.png url: https://stevenmhy.tk/ - backgroundColor: '#71BCFF' + backgroundColor: '#F4606C' textColor: '#fff' tags: - 蒟蒻 @@ -263,7 +261,8 @@ links: backgroundColor: '#34A853' textColor: '#fff' tags: - - 一个非常爱吃土豆的程序猿 + - JAVA工程师 + - Python - name: Sublime’s NoteBook avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/sublimerui.jpg @@ -275,6 +274,44 @@ links: - 嵌入式 - 蒟蒻 + - name: Singularity + avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/singularity2u.jpg + url: https://www.singularity2u.top/ + backgroundColor: '#0086F1' + textColor: '#fff' + tags: + - ACM + + - name: 暗部精英 + avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/anbujingying.jpg + url: https://anbujingying.coding.me/ + backgroundColor: '#53AECA' + textColor: '#fff' + tags: + - 蒟蒻 + - C++ + - HTML + + - name: 刘向洋 + avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/liuxiangyang.jpg + url: https://liuxiangyang.space/ + backgroundColor: '#E1524C' + textColor: '#fff' + tags: + - 运维 + - python + - Linux + + - name: 松林羊 + avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/andus.png + url: http://www.andus.top + backgroundColor: '#696969' + textColor: '#fff' + tags: + - DevOps + - Java + - 前端 + - group: 虐狗博主 icon: fas fa-heartbeat items: @@ -332,7 +369,7 @@ links: - name: 中国博客联盟 avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/zgboke.png url: https://zgboke.org/ - backgroundColor: '#53AECA' + backgroundColor: '#34A853' textColor: '#fff' tags: - 中国博客联盟 @@ -340,7 +377,7 @@ links: - name: 一站之星 avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/izstar.png url: https://www.izstar.cn/ - backgroundColor: '#3F51B5' + backgroundColor: '#F7474F' textColor: '#fff' tags: - 一站之星,学生党博客之家! @@ -351,7 +388,7 @@ links: - name: QQ交流群 avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/groups.jpg url: https://jq.qq.com/?_wv=1027&k=5F6HRuG - backgroundColor: '#04A8DF' + backgroundColor: '#0086F1' textColor: '#fff' tags: - Hugo&Hexo博客群(技术交流) @@ -359,7 +396,7 @@ links: - name: Telegram交流群 avatar: https://cdn.jsdelivr.net/gh/TRHX/ImageHosting/ITRHX-LINKS/groups.jpg url: https://t.me/hexoisthebest - backgroundColor: '#04A8DF' + backgroundColor: '#967ADC' textColor: '#fff' tags: - Hexo&Hugo博客(迫真TG分群) @@ -372,13 +409,16 @@ links: --- -* 海内存知己,天涯若比邻!相见即是缘分,欢迎各位大佬留言互换友链! -* 互换友链前请先添加本站点为友链! -* 必须要有名称、头像链接、至少一个标签或者一个简介哦~ -* 一段时间内无法访问贵站将会被分组到【404 NOT FOUND】,请及时恢复站点! +> * 海内存知己,天涯若比邻!相见即是缘分,欢迎各位大佬留言互换友链! +> * 互换友链前请先添加本站点为友链!若单方面删除本站友链,本站将不再保留贵站友链! +> * 一段时间内无法访问贵站将会被分组到【404 NOT FOUND】,请及时恢复站点! +> * 留言请告诉我你的名称、主页、头像、标签或者简介哦~ +> * 如果想定制你的卡片颜色,也可以留言告诉我哦~(十六进制颜色码,如:#FF0000) + +--- > 名称:TRHX'S BLOG > 主页:https://www.itrhx.com/ > 头像:https://www.itrhx.com/images/trhx.png > 标签:Python、爬虫、前端 -> 简介:求知若饥 虚心若愚 +> 简介:求知若饥,虚心若愚! diff --git a/themes/material-x-1.2.1/_config.yml b/themes/material-x-1.2.1/_config.yml index f6686399b..1ff0f2ea4 100644 --- a/themes/material-x-1.2.1/_config.yml +++ b/themes/material-x-1.2.1/_config.yml @@ -4,11 +4,11 @@ info: docs: https://xaoxuu.com/wiki/material-x/ cdn: # 把对应的那一行注释掉就使用本地的文件 css: - style: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/css/style.css + style: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.2.1/css/style.css js: - app: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/js/app.js - search: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/js/search.js - volantis: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/js/volantis.min.js + app: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.2.1/js/app.js + search: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.2.1/js/search.js + volantis: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.2.1/js/volantis.min.js @@ -33,12 +33,12 @@ music: mode: circulation # random (随机) single (单曲) circulation (列表循环) order (列表) server: netease # netease(网易云音乐)tencent(QQ音乐) xiami(虾米) kugou(酷狗) type: playlist # song (单曲) album (专辑) playlist (歌单) search (搜索) - id: 2625522030 # 歌曲/专辑/歌单 ID + id: 3019271605 # 歌曲/专辑/歌单 ID volume: 0.7 # 音量, 0~1 autoplay: false # 自动播放 # 友链页头像占位图 -avatar_placeholder: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/images/bitmap.gif +avatar_placeholder: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.2.1/images/bitmap.gif # 日期格式 http://momentjs.com/docs/ date_format: 'YYYY-MM-DD' # 文章发布日期的格式 @@ -50,8 +50,8 @@ backstretch: duration: 5000 # 持续时间(毫秒) fade: 1500 # 渐变(毫秒) images: - #- https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/images/background/017.webp - - https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/images/background/021.webp + #- https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.2.1/images/background/017.webp + - https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.2.1/images/background/021.webp ############################### 自定义 ############################### @@ -60,7 +60,7 @@ cover: scheme: search # 后期将会提供多种封面方案 # height: half # full(默认值): 首页封面占据整个第一屏幕,其他页面占半个屏幕高度, half: 所有页面都封面都只占半个屏幕高度 # title: "TRHX'S BLOG" - logo: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/images/logo.png # logo和title只显示一个,若同时设置,则只显示logo + logo: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.2.1/images/logo.png # logo和title只显示一个,若同时设置,则只显示logo search_placeholder: '世界之大,探索一下!' # 主页封面菜单 features: @@ -147,7 +147,7 @@ body: [article, comments] # 侧边栏小部件,默认按下面给定的顺序全部显示,文章中还可以自定义显示一部分以及顺序 sidebar: - widget: author - avatar: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.1.9/images/trhx.png + avatar: https://cdn.jsdelivr.net/gh/TRHX/CDN-for-itrhx.com@2.2.1/images/trhx.png title: TRHX'S BLOG body: 一入IT深似海,从此学习无绝期 social: true @@ -195,7 +195,7 @@ sidebar: - widget: plain icon: fas fa-comments title: 交流群组 - body: 'QQ 交流群Telegram 交流群' + body: 'QQ 交流群Telegram 交流群' # - widget: list # icon: fas fa-link # title: 特别链接 @@ -229,12 +229,12 @@ sidebar: title: "最近在听" more: icon: far fa-heart - url: https://music.163.com/#/playlist?id=2415512873 + url: https://music.163.com/#/user/home?id=1379654769 rel: external nofollow noopener noreferrer target: _blank server: netease # netease(网易云音乐)tencent(QQ音乐) xiami(虾米) kugou(酷狗) type: playlist # song (单曲) album (专辑) playlist (歌单) search (搜索) - id: 2625522030 # 歌曲/专辑/歌单 ID + id: 3019271605 # 歌曲/专辑/歌单 ID # 社交信息 social: diff --git a/themes/material-x-1.2.1/layout/_partial/article.ejs b/themes/material-x-1.2.1/layout/_partial/article.ejs index 18c1d9c72..3e0273977 100644 --- a/themes/material-x-1.2.1/layout/_partial/article.ejs +++ b/themes/material-x-1.2.1/layout/_partial/article.ejs @@ -29,7 +29,7 @@ <% var items = []; post.prev.tags.each(function(item){ - items.push('' + item.name + ''); + items.push(' ' + item.name + ''); }); %>
    @@ -56,7 +56,7 @@ <% var items = []; post.next.tags.each(function(item){ - items.push('' + item.name + ''); + items.push(' ' + item.name + ''); }); %>
    diff --git a/themes/material-x-1.2.1/layout/_partial/footer.ejs b/themes/material-x-1.2.1/layout/_partial/footer.ejs index 4018c472d..7ea9f5269 100644 --- a/themes/material-x-1.2.1/layout/_partial/footer.ejs +++ b/themes/material-x-1.2.1/layout/_partial/footer.ejs @@ -51,7 +51,7 @@ BY-NC-SA 4.0  |   --> 站点地图  |   - 站长统计

    + 站长统计

    PoweredHexo diff --git a/themes/material-x-1.2.1/layout/_partial/post.ejs b/themes/material-x-1.2.1/layout/_partial/post.ejs index bef7e1c6b..8e5a35b61 100644 --- a/themes/material-x-1.2.1/layout/_partial/post.ejs +++ b/themes/material-x-1.2.1/layout/_partial/post.ejs @@ -15,7 +15,7 @@ <% if (post.tags && post.tags.length) { %>
    <% post.tags.each(function(item){ %> - <%=item.name %> +  <%=item.name %> <%})%>
    <% } %> diff --git a/themes/material-x-1.2.1/source/less/_article.less b/themes/material-x-1.2.1/source/less/_article.less index f0c36c115..8a2a38eb7 100644 --- a/themes/material-x-1.2.1/source/less/_article.less +++ b/themes/material-x-1.2.1/source/less/_article.less @@ -4,6 +4,9 @@ line-height: @lineheight_base; word-break: break-all; word-wrap: break-word; + @media (max-width: @on_phone) { + font-size: @fontsize_base_phone; + } img { position: relative; margin: 0 auto; @@ -62,7 +65,7 @@ list-style: initial; padding-left: 10px; margin-left: 10px; - margin-bottom: 1em; + /*margin-bottom: 1em;*/ &.center{ justify-content: center; } diff --git a/themes/material-x-1.2.1/source/less/_base.less b/themes/material-x-1.2.1/source/less/_base.less index 87d79ef10..21e6269b4 100644 --- a/themes/material-x-1.2.1/source/less/_base.less +++ b/themes/material-x-1.2.1/source/less/_base.less @@ -68,7 +68,7 @@ fancybox{ .title, .logo{ font-size: @fontsize_h1*3; line-height: @fontsize_h1*3.3; - margin-top: ~"calc(28vh - 2*@{gap})"; + margin-top: ~"calc(28vh - 1*@{gap})"; text-align: center; font-weight: bold; } @@ -112,6 +112,11 @@ fancybox{ left: @iconMargin+1px; font-size: @fontsize_base; color: fade(@color_text_main, 60%); + @media(max-width: @on_phone){ + font-size: @fontsize_base * 0.8; + height: @searchbar_height_cover * 0.8; + line-height: @searchbar_height_cover * 0.8; + } } .input { display: block; @@ -126,6 +131,8 @@ fancybox{ padding-left: @iconW + @iconMargin; @media(max-width: @on_phone){ padding-left: @iconW + @iconMargin; + font-size: @fontsize_base * 0.8; + height: @searchbar_height_cover * 0.8; } border-radius: @height_navbar; background: @color_bg_code_block; diff --git a/themes/material-x-1.2.1/source/less/_color.less b/themes/material-x-1.2.1/source/less/_color.less index 8fa636efe..ecafb16c7 100644 --- a/themes/material-x-1.2.1/source/less/_color.less +++ b/themes/material-x-1.2.1/source/less/_color.less @@ -33,9 +33,9 @@ // 标题文字颜色(h3) @color_text_h3: darken(@color_text_main, 20%); // 链接颜色 -@color_text_link: @color_md_deep_orange; +@color_text_link: @theme_main; // 链接高亮颜色 -@color_text_highlight: @theme_main; +@color_text_highlight: @color_md_deep_orange; // 在主题色中显示的文本(一般为白或深灰) @color_text_in_header: @white; // 正文文字颜色 diff --git a/themes/material-x-1.2.1/source/less/_fonts.less b/themes/material-x-1.2.1/source/less/_fonts.less index f17af76f3..e259ca96b 100644 --- a/themes/material-x-1.2.1/source/less/_fonts.less +++ b/themes/material-x-1.2.1/source/less/_fonts.less @@ -27,7 +27,8 @@ // 字号 // base -@fontsize_base: 17px; +@fontsize_base: 16px; +@fontsize_base_phone: 14px; @fontsize_small: @fontsize_base * 0.875; @fontsize_footnote: @fontsize_base * 0.7; @lineheight_base: 1.7; @@ -39,5 +40,5 @@ @fontsize_h5: @fontsize_base * 1; @fontsize_h6: @fontsize_small; // article -@fontsize_article_title: @fontsize_h1 * 1.2; -@fontsize_article_title_phone: @fontsize_h2; +@fontsize_article_title: @fontsize_h1 * 1.1; +@fontsize_article_title_phone: @fontsize_h2 * 0.9; diff --git a/themes/material-x-1.2.1/source/less/_main.less b/themes/material-x-1.2.1/source/less/_main.less index a363eae8c..0020c0489 100644 --- a/themes/material-x-1.2.1/source/less/_main.less +++ b/themes/material-x-1.2.1/source/less/_main.less @@ -676,7 +676,7 @@ a.goroups_button_new { padding: 15px 27px 13px; margin: 10px auto; text-transform: uppercase; - width: @width_sidebar * 0.8; + width: 90%; } a.goroups_button_new:hover { background: #04A8DF; -- GitLab