爬虫 第五天

Posted bky20061005

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了爬虫 第五天相关的知识,希望对你有一定的参考价值。

爬虫 第五天

1. js 解密,混淆,逆向

  • url:https://www.aqistudy.cn/html/city_detail.html
  • 分析:
    • 空气指标的数据是动态加载出来
      • 修改了搜索条件后点击搜索按钮会发起ajax请求,请求到我们想要的指标数据。
    • 从上一步定位到的数据包中提取出url,请求方式,请求参数
      • url和请求方式可以拿来直接用
      • 请求参数是动态变化且加密
    • 响应数据也是加密的密文数据
  • 找到点击搜索按钮发起的ajax请求对应的代码
    • 基于火狐浏览器的开发者工具找到搜索按钮对应点击事件绑定的js函数是哪个
      • getData(),该函数的实现
        • type=HOUR:以小时为单位进行数据的查询
        • 调用了另两个函数:getAQIData(), getWeatherData()
        • 并没有找到ajax请求对应的的代码
      • 分析getAQIData&getWeatherData:
        • 这两个函数的实现几乎一致,唯一的区别是
          • var method = ‘GETDETAIL‘;
          • var method = ‘GETCITYWEATHER‘;
        • 也没有找到ajax请求对应的代码,但是发现了另一个函数的调用:
          • getServerData(method, param, function(obj),0.5 )
            • method:
              • ‘GETDETAIL‘
              • ‘GETCITYWEATHER‘
            • param是一个字典,有四组键值对:
              • city;
              • type;
              • startTime;
              • endTime;
        • 分析getServerData函数的实现:
          • 基于抓包工具进行全局搜索,定位到了一个指定的数据包,出现了getServerData关键词,这个关键词对应的js代码被加密了
          • JS混淆:将js中的核心代码加密
          • JS反混淆:
            • 暴力破解:
              • url:https://www.bm8.com.cn/jsConfusion/
            • 分析反混淆后的getServerData函数的实现代码:
              • 终于发现了ajax请求对应的代码:
                • getParam(method, object)返回动态变化且加密的请求参数d的值。
                  • method == method
                  • object == param
                • decodeData(data):接受加密的响应数据返回解密后的明文数据
                  • data:加密的响应数据
        • js逆向:
          • 自动逆向:
            • PyExecJS介绍:PyExecJS 是一个可以使用 Python 来模拟运行 javascript 的库。
              • 一定实现在本机装好nodejs的开发环境
            • 我们需要pip install PyExecJS对其进行环境安装。

In [3]:

#模拟执行js函数获取动态变化且解密的请求参数d的值
import execjs
node = execjs.get()
 
# Params
method = 'GETCITYWEATHER'
city = '北京'
type = 'HOUR'
start_time = '2018-01-25 00:00:00'
end_time = '2018-01-25 23:00:00'
 
# Compile javascript
file = 'test.js'
ctx = node.compile(open(file,encoding='utf-8').read())
 
# Get params
js = 'getPostParamCode("{0}", "{1}", "{2}", "{3}", "{4}")'.format(method, city, type, start_time, end_time)
params = ctx.eval(js)#模拟执行指定的js函数
print(params)
tdgHOYxwKdDSgYXe+RLPzYCgLvrddahasI5XXklB4gVLYqab+XRPpMD/oSqnJ/aEmFwzVEUhLnPzRy03+X1BI4qc9EYeRPqiKrT+f1JQExGQ4ii8kKvZhGH+nPffaX/xq5iLB6vblcvBC/L8e6UxdnHlajfkXrLQf1qv5Hcg3c++RoGxPAMOgNc6HbCbQG2sE6yemJ7l8HI9CyNktTP7AwQC04bTbY+s+o7lljhqUvsyMZq88MU1VV46TFExCP7vxfmEl6YFeV892bU27lPedTCtSnYbCEfFCJDP0DfEBHe0XFOcgXs+Yl5h58efciX69k9IEvGCKenhokOJQ2tS178anRoT37sEBV5cZeLY8Uzh8UUWgxg2sH+JJsg8ARclHhK0AN/SA4wFy8XmwdBun1zHxV8LoPfn3cxqzXnNKOp/nowpNnbyuMSZtftbf41HB1dEdkm07a2LzCaJgUEpPmLZUuA7+lDlCKqTsEZVh9w=
  • 对ajax的url携带d请求参数进行post请求的发送可以获取加密的响应数据

In [5]:

import execjs
import requests

node = execjs.get()
 
# Params
method = 'GETCITYWEATHER'
city = '北京'
type = 'HOUR'
start_time = '2018-01-25 00:00:00'
end_time = '2018-01-25 23:00:00'
 
# Compile javascript
file = 'test.js'
ctx = node.compile(open(file,encoding='utf-8').read())
 
# Get params
js = 'getPostParamCode("{0}", "{1}", "{2}", "{3}", "{4}")'.format(method, city, type, start_time, end_time)
params = ctx.eval(js)

#发起post请求
url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'
response_text = requests.post(url, data={'d': params}).text
print(response_text)
eAkzHZvdWqslCrP29e8XgEP22qdvyxus1TrEFB8uvsD0ChwbOTBCJErsCqVJyLQJ9wdhdK9lk3nl/SEeVqoXSY48w11ODT7v6rhQkkXuZ3Vv+VOQ7C7zXtLvbJJDIq9Nu3RRA+8rS/R0lnyMUk98IQ==

In [12]:

#将密文的响应数据进行解密:模拟调用decodeData(data)
import execjs
import requests

node = execjs.get()
 
# Params
method = 'GETCITYWEATHER'
city = '北京'
type = 'HOUR'
start_time = '2018-01-25 00:00:00'
end_time = '2018-01-25 23:00:00'
 
# Compile javascript
file = 'test.js'
ctx = node.compile(open(file,encoding='utf-8').read())
 
# Get params
js = 'getPostParamCode("{0}", "{1}", "{2}", "{3}", "{4}")'.format(method, city, type, start_time, end_time)
params = ctx.eval(js)

#发起post请求
url = 'https://www.aqistudy.cn/apinew/aqistudyapi.php'
response_text = requests.post(url, data={'d': params}).text
# #对加密的响应数据进行解密
jss = 'decodeData("{0}")'.format(response_text)
print(jss)
decrypted_data = ctx.eval(jss)
print(decrypted_data)
decodeData("S+PG+bQmwr20q9LEnYxZb6d9kwGuj+GKqm/YqBW0N9VTTsfFzS6mR86ne1uxqNuepTIfI+opvFV/np093XWIf2IXLkXoN7yUEFNnINrJBIN9MFj2Y9rWgCXZXe4k0PtMub9YKalryHwuO7IlNJN3OA==")
---------------------------------------------------------------------------
ProgramError                              Traceback (most recent call last)
<ipython-input-12-775eb2217305> in <module>()
     26 jss = 'decodeData("{0}")'.format(response_text)
     27 print(jss)
---> 28 decrypted_data = ctx.eval(jss)
     29 print(decrypted_data)

~Anaconda3libsite-packagesexecjs\_abstract_runtime_context.py in eval(self, source)
     25         if not self.is_available():
     26             raise execjs.RuntimeUnavailableError
---> 27         return self._eval(source)
     28 
     29     def call(self, name, *args):

~Anaconda3libsite-packagesexecjs\_external_runtime.py in _eval(self, source)
     76 
     77             code = 'return eval({data})'.format(data=data)
---> 78             return self.exec_(code)
     79 
     80         def _exec_(self, source):

~Anaconda3libsite-packagesexecjs\_abstract_runtime_context.py in exec_(self, source)
     16         if not self.is_available():
     17             raise execjs.RuntimeUnavailableError
---> 18         return self._exec_(source)
     19 
     20     def eval(self, source):

~Anaconda3libsite-packagesexecjs\_external_runtime.py in _exec_(self, source)
     86             else:
     87                 output = self._exec_with_pipe(source)
---> 88             return self._extract_result(output)
     89 
     90         def _call(self, identifier, *args):

~Anaconda3libsite-packagesexecjs\_external_runtime.py in _extract_result(self, output)
    165                 return value
    166             else:
--> 167                 raise ProgramError(value)
    168 
    169 

ProgramError: Error: Malformed UTF-8 data

selenium

回顾

  • 单线程+多任务的异步协程
    • 特殊的函数
      • 调用后实现内部的程序语句不会被立即执行
      • 调用后返回一个协程对象
    • 协程对象
      • 协程==特殊函数==一组指定的操作
    • 任务对象
      • 高级的协程对象
      • 绑定回调函数
        • task.add_done_callback(func)
        • func:
          • 必须要有一个参数(当前的任务对象)
          • 参数.result()表示的就是特殊函数的返回值
      • 任务对象 == 一组指定的操作
    • 事件循环对象
      • 创建一个eventloop对象
      • 作用:
        • 必须要装载一个或者多个任务对象(任务对象是需要注册到eventloop)
        • 启动事件循环对象
          • 可以异步的执行其内部注册的每一个任务对象对应的指定操作
    • 等待await:确保eventloop一定会执行阻塞操作
    • 挂起:让当前发生阻塞的任务对象交出cpu的使用权
      • asyncio.wait(tasks)
    • 重点:
      • 特殊函数内部不可以出现不支持异步模块的代码
    • aiohttp:支持异步的网络请求模块
      • 使用使用上下文机制(with...as)
      • 实例化一个请求对象(ClientSession())
      • get/post()进行请求发送。(阻塞操作)
      • 获取响应数据(阻塞操作)
        • response.text():字符串
        • response.read():byte
  • selenium
    • 动作链
    • 无头浏览器
    • 规避检测
      • 浏览器托管
    • 12306的模拟登陆
  • 动作链
    • from selenium.webdriver import ActionChains
    • NoSuchElementException:没有定位到指定的标签
      • 定位的标签是存在于一张嵌套的子页面中,如果想定位子页面中的指定标签的话需要:
        • bro.switch_to.frame(‘iframe标签id的属性值‘):将当前浏览器页面切换到指定的子页面的范围中
    • 针对指定的浏览器实例化一个动作链对象
      • action = ActionChains(bro)
    • action.click_and_hold(tagName)
    • move_by_offset(10,15)
    • perform()是的动作链立即执行

In [6]:

from selenium import webdriver
from selenium.webdriver import ActionChains
from time import sleep
# 后面是你的浏览器驱动位置,记得前面加r'','r'是防止字符转义的
bro = webdriver.Chrome('./chromedriver.exe')
bro.get('https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable')

#标签定位
bro.switch_to.frame('iframeResult')
div_tag = bro.find_element_by_id('draggable')

#需要使用ActionChains定制好的行为动作
action = ActionChains(bro)#针对当前浏览器页面实例化了一个动作链对象
action.click_and_hold(div_tag)#点击且长按指定的标签

for i in range(1,7):
    action.move_by_offset(10,15).perform()#perform()是的动作链立即执行
    sleep(0.5)
    
bro.quit()
    
  • 无头浏览器
    • 没有可视化界面的浏览器
    • phantomJS:无头浏览器
    • 谷歌无头浏览器:
      • 就是你本机安装的谷歌浏览器,只是需要通过代码进行相关配置就可以变成无头浏览器

In [8]:

from selenium.webdriver.chrome.options import Options
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')



bro = webdriver.Chrome(executable_path='./chromedriver.exe',chrome_options=chrome_options)
bro.get('https://www.baidu.com/')
sleep(1)
bro.save_screenshot('./1.png')#截屏

print(bro.page_source)
  • selenium规避检测
    • 浏览器托管
      • 环境配置:
        • 1.本机谷歌浏览器驱动程序所在的目录的路径添加到环境变量中
        • 2.使用本机谷歌的驱动程序开启一个浏览器
          • chrome.exe --remote-debugging-port=9222 --user-data-dir="C:selenumAutomationProfile"
            • 9222:端口(任意)
            • "C:selenumAutomationProfile":已经事先存在的一个空目录
      • 使用如下代码接管目前打开的浏览器:

In [30]:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
 
chrome_options = Options()
chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")


bro = webdriver.Chrome(executable_path='./chromedriver.exe',chrome_options=chrome_options)#代码托管代开的浏览器,不会实例化一个新的浏览器。
bro.get('https://kyfw.12306.cn/otn/login/init')
C:UserslaonanhaiAnaconda3libsite-packagesipykernel_launcher.py:8: DeprecationWarning: use options instead of chrome_options
  

12306模拟登陆

  • url:https://kyfw.12306.cn/otn/login/init
  • 分析:
    • 识别的验证码图片必须通过截图获取然后存储到本地
      • 登陆操作和唯一的验证码图片一一对应

In [25]:

#pip install Pillow
from PIL import Image
from selenium.webdriver import ActionChains
from selenium import webdriver
#识别验证码的函数
def transformCode(imgPath,imgType):
    chaojiying = Chaojiying_Client('13614167787', '13614167787', '903126')
    im = open(imgPath, 'rb').read()
    return chaojiying.PostPic(im,imgType)['pic_str']

In [26]:

bro = webdriver.Chrome(executable_path='./chromedriver.exe')
bro.get('https://kyfw.12306.cn/otn/login/init')
sleep(2)
bro.find_element_by_id('username').send_keys('xxxxxxx')
bro.find_element_by_id('password').send_keys('12345465')

#验证码的点击操作
bro.save_screenshot('main.png')#将页面当做图片保存到本地
#将单独的验证码图片从main.png中裁剪下载
img_tag = bro.find_element_by_xpath('//*[@id="loginForm"]/div/ul[2]/li[4]/div/div/div[3]/img')#将验证码图片的标签定位到了
location = img_tag.location
size = img_tag.size
# print(location,size)
#裁剪的范围(验证码图片左下角和右上角两点坐标)
rangle = (int(location['x']),int(location['y']),int(location['x']+size['width']),int(location['y']+size['height']))
#使用Image类根据rangle裁剪范围进行验证码图片的裁剪
i = Image.open('./main.png')
frame = i.crop(rangle)#验证码对应的二进制数据
frame.save('./code.png')

result = transformCode('./code.png',9004)#99,71|120,140
#99,71|120,140 == [[99,71],[120,140]]
all_list = []#[[99,71],[120,140]]
if '|' in result:
    list_1 = result.split('|')
    count_1 = len(list_1)
    for i in range(count_1):
        xy_list = []
        x = int(list_1[i].split(',')[0])
        y = int(list_1[i].split(',')[1])
        xy_list.append(x)
        xy_list.append(y)
        all_list.append(xy_list)
else:
    x = int(result.split(',')[0])
    y = int(result.split(',')[1])
    xy_list = []
    xy_list.append(x)
    xy_list.append(y)
    all_list.append(xy_list)
    
for data in all_list:
    x = data[0]#11
    y = data[1]#22
    ActionChains(bro).move_to_element_with_offset(img_tag,x,y).click().perform()
    sleep(1)
    
sleep(2)
bro.find_element_by_id('loginSub').click()

bro.quit()

In [21]:

#下载好的示例代码
#!/usr/bin/env python
# coding:utf-8

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()


# if __name__ == '__main__':
#   chaojiying = Chaojiying_Client('超级鹰用户名', '超级鹰用户名的密码', '96001')  #用户中心>>软件ID 生成一个替换 96001
#   im = open('a.jpg', 'rb').read()                                                 #本地图片文件路径 来替换 a.jpg 有时WIN系统须要//
#   print chaojiying.PostPic(im, 1902)                                              #1902 验证码类型  官方网站>>价格体系 3.4+版 print 后要加()

以上是关于爬虫 第五天的主要内容,如果未能解决你的问题,请参考以下文章

python爬虫第五天

python学习第八十五天:网络爬虫之数据解析方式

Scrapy见面第五天

Scrapy见面第五天

十分宠爱-冲刺日志(第五天)

scrapy按顺序启动多个爬虫代码片段(python3)