乱码三千 – 分享实用IT技术

乱码三千 – 码出一个新世界


  • 首页

  • 归档

  • 搜索

Web自动化框架selenium的介绍与使用

发表于 2021-12-02

介绍

selenium 是一个 web 的自动化测试工具,可以模拟人工Web浏览,, 适合用于自动化处理和爬虫任务, 该框架支持C、 java、ruby、python等多种语言, 支持三大操作系统和各种常见浏览器

官方文档:

  • https://selenium-python.readthedocs.io/index.html
  • https://seleniumhq.github.io/selenium/docs/api/py/api.html

接下来以python为例, 给大家介绍selenium 的具体使用

selenium使用前准备

  1. 框架安装

    1
    pip install selenium
  2. web驱动安装

    推荐下载使用谷歌或火狐浏览器, 然后根据浏览器版本安装与之对应版本的驱动, 这里我使用的是谷歌浏览器

    打开浏览器设置, 查看浏览器版本:

    在这里插入图片描述

    然后下载驱动:

    ChromeDriver下载地址

    在这里插入图片描述

    谷歌123之后的版本驱动下载地址

    image-20240417115034597

    将下载的驱动解压到以下目录:

    1
    2
    Win:复制webdriver到Python安装目录下
    Mac:复制webdriver到/usr/local/bin目录下

    如果是火狐浏览器同理,以下是两大类浏览器驱动下载备用链接:

    Chrome ( chromedriver ) Firefox ( geckodriver )
    官方下载 官方下载
    淘宝镜像 淘宝镜像
    备用下载 备用下载

    接下来 我们开始引入框架并使用

  3. 框架引入

    1
    from selenium import webdriver

开始使用

  1. 自动打开浏览器访问某个网页

    1
    2
    3
    4
    from selenium import webdriver

    browser = webdriver.Chrome()
    browser.get('http://www.baidu.com/')
  2. 获取网页的源码

    1
    2
    3
    4
    5
    from selenium import webdriver

    browser = webdriver.Chrome()
    browser.get('http://www.baidu.com/')
    print(browser.page_source)
  3. 不打开浏览器获取网页源码

    很多情况下我们只需要让程序在后台静默执行就行, 不需要频繁开启浏览器, 那么我们需要对驱动进行相关设置, 如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    from selenium import webdriver
    chrome_options = webdriver.ChromeOptions()
    # 使用headless无界面浏览器模式
    chrome_options.add_argument('--headless') #增加无界面选项
    chrome_options.add_argument('--disable-gpu') #如果不加这个选项,有时定位会出现问题

    browser = webdriver.Chrome(chrome_options=chrome_options)
    browser.get('http://www.baidu.com/')
    print(browser.page_source)
  4. 以指定配置启动浏览器

    常用浏览器有常用的一些设置, 如果我们想在自动化的过程中加载进来可以使用add_argument方法, 如下:

    1
    2
    3
    4
    5
    #coding=utf-8
    from selenium import webdriver
    option = webdriver.ChromeOptions()
    option.add_argument('--user-data-dir=C:\Users\Administrator\AppData\Local\Google\Chrome\User Data') #指定数据目录
    driver=webdriver.Chrome(chrome_options=option)
  5. 指定驱动的目录

    除了将下载的驱动存放到系统指定目录使用之外, 我们还可以直接指定驱动的存放路径, 方便我们临时测试多个不同的驱动效果, 配置方法如下:

    1
    2
    3
    driver_path = r'D:\ProgramApp\chromedriver\chromedriver.exe'
    # 初始化一个driver,并且指定chromedriver的路径
    driver = webdriver.Chrome(executable_path=driver_path)
  6. webdriver常用api汇总

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    browser = webdriver.Chrome()

    browser.get_cookies() #获取所有的cookies
    browser.page_source #获取网页源码
    browser.delete_all_cookies() #删除所有cookies
    browser.delete_cookie(key) #删除某个cookies
    browser.implicitly_wait(10) #设置超时时间
    browser.execute_script("window.open('https://www.douban.com/')") #打开一个新页面
    browser.title #获取网页标题
    browser.current_url #获取当前网页url
    browser.capabilities['version'] #获取浏览器版本号
    browser.maximize_window() #浏览器最大化
    browser.minimize_window() #浏览器最小化
    browser.set_window_size(480, 800) #设置浏览器宽高
    browser.forword() # 页面前进
    browser.back() #页面后退
    browser.close() #关闭当前页面
    browser.quit() #退出整个浏览器

    注意: 如果访问一些需要有cookie验证的页面,我们可以先访问主页,然后再访问详情页,webdriver会自动携带cookie

  7. ChromeOptions常用函数汇总

    1
    2
    #设置代理ip
    options.add_argument("--proxy-server=http://110.73.2.248:8123")
  1. 网页元素定位查找

    • 通过元素ID进行定位

      1
      browser.find_element_by_id()
    • 通过标签名的方式进行定位

      1
      browser.find_element_by_tag_name("input")
    • 通过class的方式进行定位

      1
      browser.find_element_by_class_name("xxx")
    • 通过css方式定位

      1
      browser.find_element_by_css_selector("xxx")
  1. 事件模拟

    主要分为键盘事件和鼠标事件

    1. 键盘事件

      通过 send_keys()调用键盘按键, 比如:

      1
      2
      3
      4
      from selenium.webdriver.common.keys import Keys

      send_keys(Keys.TAB) # TAB
      send_keys(Keys.ENTER) # 回车

      参考代码如下:

      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
      #coding=utf-8 
      from selenium import webdriver
      from selenium.webdriver.common.keys import Keys #需要引入 keys 包
      import os,time

      driver = webdriver.Firefox()
      driver.get("http://passport.kuaibo.com/login/?referrer=http%3A%2F%2Fwebcloud .kuaibo.com%2F")

      time.sleep(3)
      driver.maximize_window() # 浏览器全屏显示

      driver.find_element_by_id("user_name").clear()
      driver.find_element_by_id("user_name").send_keys("fnngj")

      #tab 的定位相相于清除了密码框的默认提示信息,等同上面的 clear()
      driver.find_element_by_id("user_name").send_keys(Keys.TAB)
      time.sleep(3)
      driver.find_element_by_id("user_pwd").send_keys("123456")

      #通过定位密码框,enter(回车)来代替登陆按钮
      driver.find_element_by_id("user_pwd").send_keys(Keys.ENTER)

      #也可定位登陆按钮,通过 enter(回车)代替 click()
      driver.find_element_by_id("login").send_keys(Keys.ENTER)
      time.sleep(3)

      driver.quit()

      键盘组合键的用法:

      1
      2
      #ctrl+a 全选输入框内容 
      driver.find_element_by_id("kw").send_keys(Keys.CONTROL,'a')
      1
      2
      #ctrl+x 剪切输入框内容 
      driver.find_element_by_id("kw").send_keys(Keys.CONTROL,'x')
  1. 鼠标事件

    鼠标事件一般包括鼠标右键、双击、拖动、移动鼠标到某个元素上等等。 需要引入ActionChains类

    引入方法:

    1
    from selenium.webdriver.common.action_chains import ActionChains

    ActionChains 常用方法:

    1
    2
    3
    4
    5
    6

    perform() 执行所有ActionChains 中存储的行为;
    context_click() 右击;
    double_click() 双击;
    drag_and_drop() 拖动;
    move_to_element() 鼠标悬停。

    鼠标双击示例:

    1
    2
    3
    4
    #定位到要双击的元素
    qqq =driver.find_element_by_xpath("xxx")
    #对定位到的元素执行鼠标双击操作
    ActionChains(driver).double_click(qqq).perform()

    鼠标拖放示例:

    1
    2
    3
    4
    5
    6
    #定位元素的原位置 
    element = driver.find_element_by_name("source")
    #定位元素要移动到的目标位置
    target = driver.find_element_by_name("target")
    #执行元素的移动操作
    ActionChains(driver).drag_and_drop(element, target).perform()
  1. 特殊定位

    1. iframe定位

      1
      2
      #先找到到 ifrome1(id = f1)
      browser.switch_to_frame("f1")
    2. 内嵌窗口定位:

      1
      browser.switch_to_window("f1")

附加内容

  1. Python Webdriver Exception速查表

    webdriver在使用过程中可能会出现各种异常,我们需要了解该异常并知道如何进行异常处理。

    异常 描述
    WebDriverException 所有webdriver异常的基类,当有异常且不属于下列异常时抛出
    InvalidSwitchToTargetException 下面两个异常的父类,当要switch的目标不存在时抛出
    NoSuchFrameException 当你想要用switch_to.frame()切入某个不存在的frame时抛出
    NoSuchWindowException 当你想要用switch_to.window()切入某个不存在的window时抛出
    NoSuchElementException 元素不存在,一般由find_element与find_elements抛出
    NoSuchAttributeException 一般你获取不存在的元素属性时抛出,要注意有些属性在不同浏览器里是有不同的属性名的
    StaleElementReferenceException 指定的元素过时了,不在现在的DOM树里了,可能是被删除了或者是页面或iframe刷新了
    UnexpectedAlertPresentException 出现了意料之外的alert,阻碍了指令的执行时抛出
    NoAlertPresentException 你想要获取alert,但实际没有alert出现时抛出
    InvalidElementStateException 下面两个异常的父类,当元素状态不能进行想要的操作时抛出
    ElementNotVisibleException 元素存在,但是不可见,不可以与之交互
    ElementNotSelectableException 当你想要选择一个不可被选择的元素时抛出
    InvalidSelectorException 一般当你xpath语法错误的时候抛出这个错
    InvalidCookieDomainException 当你想要在非当前url的域里添加cookie时抛出
    UnableToSetCookieException 当driver无法添加一个cookie时抛出
    TimeoutException 当一个指令在足够的时间内没有完成时抛出
    MoveTargetOutOfBoundsException actions的move操作时抛出,将目标移动出了window之外
    UnexpectedTagNameException 获取到的元素标签不符合要求时抛出,比如实例化Select,你传入了非select标签的元素时
    ImeNotAvailableException 输入法不支持的时候抛出,这里两个异常不常见,ime引擎据说是仅用于linux下对中文/日文支持的时候
    ImeActivationFailedException 激活输入法失败时抛出
    ErrorInResponseException 不常见,server端出错时可能会抛
    RemoteDriverServerException 不常见,好像是在某些情况下驱动启动浏览器失败的时候会报这个错

  2. Xpath&Css定位方法速查表

    描述 Xpath Css
    直接子元素 //div/a div > a
    子元素或后代元素 //div//a div a
    以id定位 //div[@id=’idValue’]//a div#idValue a
    以class定位 //div[@class=’classValue’]//a div.classValue a
    同级弟弟元素 //ul/li[@class=’first’]/following- ul>li.first + li
    属性 //form/input[@name=’username’] form input[name=’username’]
    多个属性 //input[@name=’continue’ and input[name=’continue’][type=’button
    第4个子元素 //ul[@id=’list’]/li[4] ul#list li:nth-child(4)
    第1个子元素 //ul[@id=’list’]/li[1] ul#list li:first-child
    最后1个子元素 //ul[@id=’list’]/li[last()] ul#list li:last-child
    属性包含某字段 //div[contains(@title,’Title’)] div[title*=”Title”]
    属性以某字段开头 //input[starts-with(@name,’user’)] input[name^=”user”]
    属性以某字段结尾 //input[ends-with(@name,’name’)] input[name$=”name”]
    text中包含某字段 //div[contains(text(), ‘text’)] 无法定位
    元素有某属性 //div[@title] div[title]
    父节点 //div/.. 无法定位
    同级哥哥节点 //li/preceding-sibling::div[1] 无法定位

​

​

​

本文为作者原创转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

Python爬虫框架之PyQuery的使用

发表于 2021-12-02

前言

Python之所以很适合写爬虫, 其中一个原因是拥有丰富的解析器Lib

以HTML解析为例就有XPATH,Beautiful Soup和PyQuery等等

想用哪个用哪个

那么今天我们就来介绍其中PyQuery这个解析器的用法, PyQuery的语法和jquery大同小异, 如果你熟悉jquery的使用, 那么这款解析器绝对适合你

组件安装

1
pip install pyquery

组件引入

1
from pyquery import PyQuery as pq

三个小示例

假设我们有这么一个HTML文本:

1
2
3
4
5
6
7
8
<div id="wrap">
<ul class="s_from">
我是测试文本
<link href="http://aaa.com">aaa</link>
<link href="http://bbb.com">bbb</link>
<link href="http://ccc.com">ccc</link>
</ul>
</div>

如果我们要获取div标签下的所有内容(包含div标签), 那么我们可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pyquery import PyQuery as pq
html = '''
<div id="wrap">
<ul class="s_from">
我是测试文本
<link href="http://aaa.com">aaa</link>
<link href="http://bbb.com">bbb</link>
<link href="http://ccc.com">ccc</link>
</ul>
</div>
'''
doc = pq(html)
result=doc("#wrap")
print(result)

得到的结果为:

1
2
3
4
5
6
7
8
<div id="wrap">
<ul class="s_from">
我是测试文本
<link href="http://aaa.com">aaa</link>
<link href="http://bbb.com">bbb</link>
<link href="http://ccc.com">ccc</link>
</ul>
</div>

如果我们要获取div标签下的所有元素(不包含div标签),, 那么我们可以使用children()函数, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pyquery import PyQuery as pq
html = '''
<div id="wrap">
<ul class="s_from">
我是测试文本
<link href="http://aaa.com">aaa</link>
<link href="http://bbb.com">bbb</link>
<link href="http://ccc.com">ccc</link>
</ul>
</div>
'''
doc = pq(html)
result=doc("#wrap").children()
print(result)

那么得到的结果为:

1
2
3
4
5
6
<ul class="s_from">
我是测试文本
<link href="http://aaa.com">aaa</link>
<link href="http://bbb.com">bbb</link>
<link href="http://ccc.com">ccc</link>
</ul>

如果我们要获取ul标签下的所有内容, 我们可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pyquery import PyQuery as pq
html = '''
<div id="wrap">
<ul class="s_from">
我是测试文本
<link href="http://aaa.com">aaa</link>
<link href="http://bbb.com">bbb</link>
<link href="http://ccc.com">ccc</link>
</ul>
</div>
'''
doc = pq(html)
result=doc("ul").html()
print(result)

那么得到的结果为:

1
2
3
4
我是测试文本
<link href="http://aaa.com">aaa</link>
<link href="http://bbb.com">bbb</link>
<link href="http://ccc.com">ccc</link>

通过上面三个小例子示范我们大致感受了一下PyQuery的使用, 但是依然无法深入了解, 接下来我们对其用法进行总结

使用总结

  1. 通过id获取html使用#

    1
    pq(html)("#xxx")
  2. 通过class获取html使用’.`

    1
    pq(html)(".xxx")
  3. 通过标签名获取html直接使用标签

    1
    pq(html)("div")
  4. 查找父元素使用parent()

    1
    pq(html)("#xxx").parent()
  5. 获取目标位置下包裹的所有的元素

    1
    pq(html)("#xxx").children()
  6. 获取目标位置下包裹的所有的html内容

    1
    pq(html)("#xxx").html()
  7. 获取目标位置下包裹的所有的文本内容

    1
    pq(html)("#xxx").text()

    注意: 我们要和html()与children()进行区分, 我们用一个例子来解释:

    如果使用html():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # coding:utf-8

    from pyquery import PyQuery as pq
    html = '''
    <div id="wrap">
    <ul class="s_from">
    我是测试文本
    <link href="http://aaa.com">aaa</link>
    <link href="http://bbb.com">bbb</link>
    <link href="http://ccc.com">ccc</link>
    </ul>
    </div>
    <link href="http://ddd.com">ccc</link>
    </ul>
    '''
    doc = pq(html)
    result=doc("#wrap").html()
    print(result)

    得到的结果为:

    1
    2
    3
    4
    我是测试文本
    <link href="http://aaa.com"/><p>aaa</p>
    <link href="http://bbb.com"/>bbb
    <link href="http://ccc.com"/>ccc

    如果使用children():

    1
    2
    3
    doc = pq(html)
    result=doc(".s_from").children()
    print(result)

    得到结果为:

    1
    2
    3
    <link href="http://aaa.com"/><p>aaa</p>
    <link href="http://bbb.com"/>bbb
    <link href="http://ccc.com"/>ccc

    如果使用text():

    1
    2
    3
    doc = pq(html)
    result=doc(".s_from").text()
    print(result)

    得到结果为:

    1
    2
    3
    4
    我是测试文本
    aaa
    bbb
    ccc

    一目了然

  8. 查找兄弟元素使用siblings()

    1
    pq(html)("#xxx").siblings()
  9. 将所有获取到的元素组成数组items()

    1
    pq(html)("#xxx").items()

    比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # coding:utf-8

    from pyquery import PyQuery as pq
    html = '''
    <div id="wrap">
    <ul class="s_from">
    我是测试文本
    <link href="http://aaa.com">aaa</link>
    <link href="http://bbb.com">bbb</link>
    <link href="http://ccc.com">ccc</link>
    </ul>
    </div>
    <link href="http://ddd.com">ccc</link>
    </ul>
    '''
    doc = pq(html)
    result=doc("link")
    print(result)

    那么得到的结果为:

    1
    2
    3
    4
    <link href="http://aaa.com"/>aaa
    <link href="http://bbb.com"/>bbb
    <link href="http://ccc.com"/>ccc
    <link href="http://ddd.com"/>ccc

    如果我们想对其进行遍历获取其中某一个元素, 那么可以这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # coding:utf-8

    from pyquery import PyQuery as pq
    html = '''
    <div id="wrap">
    <ul class="s_from">
    我是测试文本
    <link href="http://aaa.com">aaa</link>
    <link href="http://bbb.com">bbb</link>
    <link href="http://ccc.com">ccc</link>
    </ul>
    </div>
    <link href="http://ddd.com">ccc</link>
    </ul>
    '''
    doc = pq(html)
    result=doc("link").items()
    for i in result:
    print(i)
  10. 获取标签属性信息使用attr()

    还是上面的例子, 如果我们要获取link标签中href的值, 那么可以这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # coding:utf-8

    from pyquery import PyQuery as pq
    html = '''
    <div id="wrap">
    <ul class="s_from">
    我是测试文本
    <link href="http://aaa.com">aaa</link>
    <link href="http://bbb.com">bbb</link>
    <link href="http://ccc.com">ccc</link>
    </ul>
    </div>
    <link href="http://ddd.com">ccc</link>
    </ul>
    '''
    doc = pq(html)
    result=doc("link").items()
    for i in result:
    print(i.attr('href'))
    #或者
    print(i.attr.href)
    # 上面两种获取href的方法任选其一
  11. 精确查找使用空格

    啥意思呢, 我们还是用一个例子来说明:

    如果我们要利用层级关系精确查找, 那么可以这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # coding:utf-8

    from pyquery import PyQuery as pq
    html = '''
    <div id="wrap">
    <ul class="s_from">
    我是测试文本
    <link id="first" "href="http://aaa.com">111<p>aaa</p></link>
    <link href="http://bbb.com">bbb</link>
    <link href="http://ccc.com">ccc</link>
    </ul>
    </div>
    <link href="http://ddd.com">ccc</link>
    </ul>
    '''
    doc = pq(html)
    result=doc("#wrap .s_from #first")
    print(result)

    层级之间使用空格进行分隔, 得到的结果为:

    1
    <link id="first"/>111

    我们惊奇地发现, 查找到的内容有缺失,居然没有打印<p>aaa</p>, 如果我们将link标签改为a标签:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # coding:utf-8

    from pyquery import PyQuery as pq
    html = '''
    <div id="wrap">
    <ul class="s_from">
    我是测试文本
    <a id="first" "href="http://aaa.com">111<p>aaa</p></a>
    <link href="http://bbb.com">bbb</link>
    <link href="http://ccc.com">ccc</link>
    </ul>
    </div>
    <link href="http://ddd.com">ccc</link>
    </ul>
    '''
    doc = pq(html)
    result=doc("#wrap .s_from #first")
    print(result)

    则结果为:

    1
    <a id="first">111<p>aaa</p></a>

    你可能会说之前写的语法有问题 link标签怎么能又嵌套其他标签呢, 事实上, 你也难保会碰上诸如此类的情况, 毕竟前端鱼龙混杂, 当我们获取不到值的时候, 需要特殊注意一下

  1. 根据标签属性精确查找

    比如 我们要查找itemprop属性值为keywords的meta标签, 可以这样写:

    1
    2
    doc = pq(html)
    doc('meta[itemprop="keywords"]')

    如果需要嵌套查找 比如查找itemprop属性值为author的div标签下的itemprop属性值为name的meta标签 可以这样写:

    1
    2
    doc = pq(html)
    d('div[itemprop="author"]')('meta[itemprop="name"]')

    更多层级的嵌套也是类的写法

  2. 给元素添加class

    1
    pq(html)("#xxx").addClass('active')

    ​ 如果class已经存在, 则不重复添加

  3. 移除元素的class

    1
    pq(html)("#xxx").removeClass('active')
  4. 给元素添加css样式

    1
    pq(html)("#xxx").css('font-size','14px')
  5. 移除某个标签

    1
    pq(html)("#xxx").remove('ul')
  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
    from pyquery import PyQuery as pq
    html = '''
    <div href="wrap">
    hello nihao
    <ul class="s_from">
    asdasd
    <link class='active1 a123' href="http://asda.com"><a>helloasdadasdad12312</a></link>
    <link class='active2' href="http://asda1.com">asdadasdad12312</link>
    <link class='movie1' href="http://asda2.com">asdadasdad12312</link>
    </ul>
    </div>
    '''

    doc = pq(html)
    its=doc("link:first-child")
    print('第一个标签:%s'%its)
    its=doc("link:last-child")
    print('最后一个标签:%s'%its)
    its=doc("link:nth-child(2)")
    print('第二个标签:%s'%its)
    its=doc("link:gt(0)") #从零开始
    print("获取0以后的标签:%s"%its)
    its=doc("link:nth-child(2n-1)")
    print("获取奇数标签:%s"%its)
    its=doc("link:contains('hello')")
    print("获取文本包含hello的标签:%s"%its)
  7. 多选查找, 用逗号分隔

    1
    pq(html)("h1,h2") #表示查找h1和h2标签
  8. 过滤查找filter

    1
    pq(html).filter(".fisrt") #过滤出class为.fisrt的内容
  9. 使用find方法查找

    上面我们介绍的查找都是pq(html)("xxx") 列表形式, 除此之外我们还可以用使用调用函数的形式进行查找, 效果也是一样的:

    1
    2
    3
    pq(html)("#xxx") 
    #或者用
    pq(html).find("#xxx")

最后

以上只是该库的一部分使用方法, 一边用一边掌握, 我个人还是比较喜欢使用这一套解析库, 能解决大部分的应用场景

​

​

​

本文为作者原创转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

Android原生格斗游戏开发

发表于 2021-12-01

前言

昨天晚上做了个梦, 梦见我在沙…

梦里的事已经记不得了, 不过大半夜醒来后大脑异常清醒活跃, 居然在构思着一款格斗游戏

也不知道这灵光从何而来, 近大半年都没有玩游戏, 也没有做游戏相关的开发

不过在我内心深处 确实是有一个3A游戏梦, 话说格斗游戏, 算是我童年最爱吧

不管怎么着, 反正当时是越想越起劲, 心跳加速, 鼻子发痒, 差点误以为流了鼻血

既然这么刺激, 那么 就将想法变成现实吧, 开始着手进行游戏的开发, 这次平台我选择用Android原生, 为什么不直接使用第三方引擎, 主要还是想着把基础的东西捋一遍, 地基打好了, 上层建筑也就没什么难的

游戏架构设计

无论游戏还是普通应用, 都是由两个大部分组成的, 一个是场景界面, 另一个则是事件处理

我们只需要从这两个方面着手即可

场景界面

这款游戏的场景很简单, 只有一个场景, 和拳皇类似, 一个背景, 左右各一人, 就这么简单

事件处理

移动端的游戏, 我们肯定是需要在屏幕上加上触控的游戏摇杆, 来操作角色的行动

说到动作, 所有的角色都具备左右上下跑跳滑铲的动作, 因此这部分我们在代码实现的时候, 可以将其进行抽象到父类中, 让所有子类角色与生俱来

本文为作者原创转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

使用python对博客网站的文章进行爬取

发表于 2021-12-01

前言

说到爬取文章啥的, 最好使的肯定是python, python中有很多爬虫库可供我们使用, 方便快捷, 虽然工具很多, 但是大同小异, 我们只需要用好一个库就够了

接下来以掘金为例, 给大家演示一下如何爬取网站中的文章:

网站结构分析

调出浏览器控制台, 通过分析, 我们发现掘金和简书不同, 其网站中的文章链接全部都是通过接口动态请求的, 而非以Nginx容器静态存放

由于我们需要获取多篇文章,而不是单篇, 所以文章链接我们必须先拿到手, 然后再根据链接挨个将文章爬出来

文章链接获取

在推荐一栏通过上拉加载我们很轻松就能获取到请求的接口和参数, 我们只需要将其拷贝出来用python模拟请求即可

image-20211201163303973

请求接口:

1
https://api.juejin.cn/recommend_api/v1/article/recommend_cate_feed?aid=xxx&uuid=xxx

请求参数:

1
{"id_type":2,"sort_type":200,"cate_id":"6809637769959178254","cursor":"1","limit":20}

其中cursor字段表示页数, 我们可以循环递增这个字段的值来源源不断地获取数据

响应数据:

image-20211201163627506

这个article_id就是我们需要获取的值, 将这个值和https://juejin.cn/post/进行拼接就得到了文章的实际地址, 如下:

1
https://juejin.cn/post/7016520448204603423

好了, 分析完毕后, 直接上代码实现:

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
# coding:utf-8


import codecs
import time
import requests
import json
import sys
# 增加try except嵌套层数 避免
sys.setrecursionlimit(10000)



cache_file_name = 'temp_juejin.txt'

cache = []


def loadCache():
with codecs.open(cache_file_name, "r", "utf-8") as fr:
for line in fr:
cache.append(line)
#print(cache)
#return cache[len(cache)-1]


def startScrape():

apiUrl='https://api.juejin.cn/recommend_api/v1/article/recommend_cate_feed?aid=2608&uuid=7023196943133656589'
HEADERS = {
'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:11.0) Gecko/20100101 Firefox/11.0',

'Accept-Encoding' : 'gzip,deflate,sdch',
'Accept-Language' : 'zh-CN,zh;q=0.8',
'Content-type': 'application/json; charset=UTF-8',
'accept': 'application/json, text/plain, */*',

}
for index in range(1000):
body = {"id_type":2,"sort_type":200,"cate_id":"6809637769959178254","cursor":"1","limit":20}
body["cursor"]=str(index+1)
print(body)
r = requests.post(url=apiUrl, headers=HEADERS,data=json.dumps(body))
print(r.status_code)

res=json.loads(r.content)

with codecs.open(cache_file_name, "a", "utf-8") as f:
for item in res["data"]:
print()
id=item["article_info"]["article_id"]
link = "https://juejin.cn/post/"+id
print(link)
#if link not in cache: #判断存在或者不存在
if not any(link in s for s in cache):
cache.append(link)
f.write(link+"\n")
print("新增一条连接")
#time.sleep(10)
#切记 url不要加入换行 否则404
f.close()
time.sleep(2)




def job():

loadCache()
startScrape()




if __name__ == '__main__':
job()

执行该代码前先在同级目录下新建一个temp_juejin.txt文件, 用于存放获取到的所有文章链接, 对于初学者, 这里需要注意的是json的转换处理和请求头的设置, 如果没有使用json.dumps进行转换, 那么请求会失败, 如果请求头不加Content-type和accept或者没填对, 请求正常但是返回的不是正常的数据, 这一块是很多人极易忽视的地方

代码运行后爬取的结果如下:

image-20211201164332262

好了, 有了文章链接, 下一步我们就开始挨个文章的爬取

文章爬取

爬虫框架, 我这里使用的是PyQuery, 关于PyQuery的用法, 可参见《Python爬虫框架之PyQuery的使用》

接下来我们爬取文章的标题和内容, 代码如下:

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
# -*- coding:utf-8 -*-

import requests
from pyquery import PyQuery as pq
import codecs
import os
import sys





sys.setrecursionlimit(1000000)

# 当前文件路径
current_path = os.path.abspath(__file__)
# 父目录
father_path = os.path.abspath(os.path.dirname(current_path) + os.path.sep + ".")



headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36',
'cookie': '__cfduid=d89ead99eeea979ea1f2a1a6243d186461600935008; Hm_lvt_2374bfdfe14a279e4a045267051b54e1=1600935010,1601459114; __yjsv3_shitong=1.0_7_8b1bac638e380ca12f87734ab2405afe2e94_300_1601470572844_223.104.3.46_aaa564dc; cf_chl_1=04a386244ad8d71; cf_chl_prog=x17; cf_clearance=f4af2dbdded649a7cf11a5a52d168289e98e68c6-1601470577-0-1zd4e21871z8a534313z279abd70-150; Hm_lpvt_2374bfdfe14a279e4a045267051b54e1=1601470578',
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',

}

session = requests.Session()
session.headers.update(headers)

origin_url_cache_file_name="temp_juejin.txt"

originUrlCache = []

# 将文件中内容按行加载至列表中
def loadCache(filename, filter_text):
cache = []
with codecs.open(filename, "r", "utf-8") as fr:
if filter_text:
for line in fr:
if filter_text in line:
cache.append(line.replace("\n", ""))
else:
for line in fr:
cache.append(line.replace("\n", ""))
return cache



def get_article_by_url(url):
rep = session.get(url)
d=pq(rep.text)
title = d('h1').text()
content = d('.markdown-body').html()
return title, content

def startScrape():
for link in originUrlCache:

print(link + "\n")
title, content = get_article_by_url(link)
print(title + "\n")
print(content)





if __name__ == '__main__':

#将需要爬取的url加载到内存中
originUrlCache=loadCache("{parent}/{filename}".format(parent=father_path,filename=origin_url_cache_file_name), None)
#开始爬取文章
startScrape()

但是打印结果全部为None, 去控台一查发现掘金文章页面内容是通过js动态渲染的, 如果直接获取html是无法通过PyQuery获取到我们想要的内容的, 那这咋办?

想一下, 如果我们能拿到渲染完成后的html, 然后再通过PyQuery进行查找, 不就完事了

问题在于如何获取到渲染完成后的页面源码, 单纯的Get请求肯定是不行的, 我们需要模拟浏览器渲染才行,

这个时候我们就需要用到一个Web自动化框架, 也就是大名鼎鼎的selenium, 它可以模拟真实的浏览器访问和查找甚至是点击操作, 这里我们只需要利用它得到页面源码即可, 关于selenium的详细使用, 可参见《Web自动化框架selenium的介绍与使用》

于是代码修改成如下模样:

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
# -*- coding:utf-8 -*-

import requests
from pyquery import PyQuery as pq
import codecs
import os
import sys

from selenium import webdriver



chrome_options = webdriver.ChromeOptions()
# 使用headless无界面浏览器模式
chrome_options.add_argument('--headless') #增加无界面选项
chrome_options.add_argument('--disable-gpu') #如果不加这个选项,有时定位会出现问题

browser = webdriver.Chrome(chrome_options=chrome_options)




sys.setrecursionlimit(1000000)

# 当前文件路径
current_path = os.path.abspath(__file__)
# 父目录
father_path = os.path.abspath(os.path.dirname(current_path) + os.path.sep + ".")




origin_url_cache_file_name="temp_juejin.txt"

originUrlCache = []

# 将文件中内容按行加载至列表中
def loadCache(filename, filter_text):
cache = []
with codecs.open(filename, "r", "utf-8") as fr:
if filter_text:
for line in fr:
if filter_text in line:
cache.append(line.replace("\n", ""))
else:
for line in fr:
cache.append(line.replace("\n", ""))
return cache



def get_article_by_url(url):
browser.get(url)
d = pq(browser.page_source)
title = d('h1').text()
content = d('.markdown-body').html()
return title, content

def startScrape():
for link in originUrlCache:

print(link + "\n")
title, content = get_article_by_url(link)
print(title + "\n")
print(content)





if __name__ == '__main__':

#将需要爬取的url加载到内存中
originUrlCache=loadCache("{parent}/{filename}".format(parent=father_path,filename=origin_url_cache_file_name), None)
#开始爬取文章
startScrape()

该代码运行的前提是需要安装谷歌浏览器

运行时如果提示This version of ChromeDriver only supports Chrome version, 那么说明浏览器版本和驱动版本不一致, 需要下载与浏览器相匹配的驱动

查看谷歌浏览器版本:

在这里插入图片描述

然后下载驱动:

ChromeDriver下载地址

在这里插入图片描述

将下载的驱动解压到以下目录:

1
2
Win:复制webdriver到Python安装目录下
Mac:复制webdriver到/usr/local/bin目录下

至此, 我们成功爬取到指定文章地址的标题和内容

既然获取到了想要的数据, 那么接下来你可以考虑将其存放到本地, 或者上传到你的wordpress

关于wordpress文章的上传,可参考文章《如何将python采集到的文章保存到wordpress》

文章上传到wordpress

秉着善始善终的原则, 以上面的代码为例给大家补充上上传wordpress后的最终代码:

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
# -*- coding:utf-8 -*-

import time
from lxml import html
import requests
from pyquery import PyQuery as pq
import codecs
import os
import sys
from wordpress_xmlrpc import Client, WordPressPost
from wordpress_xmlrpc.methods.posts import GetPosts,NewPost

from selenium import webdriver
import time


chrome_options = webdriver.ChromeOptions()
# 使用headless无界面浏览器模式
chrome_options.add_argument('--headless') #增加无界面选项
chrome_options.add_argument('--disable-gpu') #如果不加这个选项,有时定位会出现问题

browser = webdriver.Chrome(chrome_options=chrome_options)

sys.setrecursionlimit(1000000)

# 当前文件路径
current_path = os.path.abspath(__file__)
# 父目录
father_path = os.path.abspath(os.path.dirname(current_path) + os.path.sep + ".")

pushed_cache_file_name="temp_juejin_pushed_url.txt"
origin_url_cache_file_name="temp_juejin.txt"




pushedCache = []
originUrlCache = []
wp = Client('http://您的域名/xmlrpc.php', 'wordpress用户名', 'wordpress登录密码')

# 将文件中内容按行加载至列表中
def loadCache(filename, filter_text):
cache = []
with codecs.open(filename, "r", "utf-8") as fr:
if filter_text:
for line in fr:
if filter_text in line:
cache.append(line.replace("\n", ""))
else:
for line in fr:
cache.append(line.replace("\n", ""))
return cache



def push_article(post_title,post_content_html):
post = WordPressPost()
post.title = post_title
post.slug = post_title
post.content = post_content_html
post.terms_names = {
'post_tag': post_title.split(" "),
'category': ["itarticle"]
}
post.post_status = 'publish'
wp.call(NewPost(post))

def get_article_by_url(url):
browser.get(url)
d = pq(browser.page_source)
browser.quit
title = d('h1').text()
content = d('.markdown-body').html()
return title, content

def startScrape():
for link in originUrlCache:
if not any(link in s for s in pushedCache):
print(link + "\n")
title, content = get_article_by_url(link)
print(title + "\n")
print(content)
push_article(title,content)
time.sleep(2)
with codecs.open("{parent}/{filename}".format(parent=father_path,filename=pushed_cache_file_name), 'a', "utf-8") as fw:
fw.write(link + "\n")
fw.close()




if __name__ == '__main__':
#将已经爬取过的url加载到内存中
pushedCache=loadCache("{parent}/{filename}".format(parent=father_path,filename=pushed_cache_file_name), None)
#将需要爬取的url加载到内存中
originUrlCache=loadCache("{parent}/{filename}".format(parent=father_path,filename=origin_url_cache_file_name), None)
#开始爬取文章
startScrape()

只要将域名, 用户名和密码替换成你自己的就行

注意wordpress_xmlrpc库的安装:

1
pip install python-wordpress-xmlrpc

补充

另外 上面获取文章链接环节也可以直接使用selenium进行获取, 虽然不如调接口来的快, 但是碰上接口被加密的情况, 那么selenium的方式能快速解决

本文为作者原创转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

越来越火的网络请求Fetch和Axios到底有什么区别

发表于 2021-11-30

在这几天由于编写脚本等一系列原因,不知怎么的突然发现现在很多主流的网站已经大量开始使用Fetch进行网络请求,感觉再不学习Fetch就要Out了,所以我花了一些时间专门去研究了一下关于Fetch的相关知识,发现Fetch被讨论的并不多,很多都是一年前甚至两年前的文章,大多数文章最后得出来的结论几乎都是Axios比Fetch好用。

事实确实如此,就我个人的体验来讲,Axios使用体验确实优于Fetch,那么为什么目前在很多大公司的网站上面都开始使用Fetch进行网络请求,带着这个疑问,我又去查找了很多资料,同时又自行将同样的请求使用Axios和Fetch进行尝试,最后得出一个结论:Fetch的优势仅仅在于浏览器原生支持。

对的,其实Fetch比起Axios来讲几乎没有任何优势(除了浏览器原生支持),Axios各个方面都比Fetch好用,Fetch要想实现Axios的一些功能还需要手动进行封装。

我截取了几个比较大的网站的请求图:

掘金:

img

YouTube:

img

知乎:

img

需要注意的是:Axios是对XMLHttpRequest的封装,而Fetch是一种新的获取资源的接口方式,并不是对XMLHttpRequest的封装。

它们最大的不同点在于Fetch是浏览器原生支持,而Axios需要引入Axios库。

1. 火热程度

虽然无法进行直观的比较,但是我们可以从npm包下载量来看:

img

因为Node环境下默认是不支持Fetch的,所以必须要使用node-fetch这个包,而这个包的周下载量一路攀升,可以看到已经来到了每周2千多万的下载量。这还仅仅是Node环境下,浏览器则是默认支持不需要第三方包。

img

上面是Axios的下载量,可以看到也是一路攀升,Axios封装的各种方法确实非常的好用。


本篇文章着重会从下面几项内容进行比较:

  • 兼容性
  • 基本语法
  • 响应超时
  • 对数据的转化
  • HTTP拦截器
  • 同时请求

2. 兼容性问题

Axios可以兼容IE浏览器,而Fetch在IE浏览器和一些老版本浏览器上没有受到支持,但是有一个库可以让老版本浏览器支持Fetch即它就是whatwg-fetch,它可以让你在老版本的浏览器中也可以使用Fetch,并且现在很多网站的开发都为了减少成本而选择不再兼容IE浏览器。

注意:在比较旧的浏览器上面可能还需要使用promise兼容库。

各个浏览器对Fetch的兼容:

img

3. 请求方式

下面我们来看一下如何使用Axios和Fetch进行请求。

Axios:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const options = {
url: "http://example.com/",
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
data: {
a: 10,
b: 20,
},
};

axios(options).then((response) => {
console.log(response.status);
});

Fetch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const url = "http://example.com/";
const options = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
body: JSON.stringify({
a: 10,
b: 20,
}),
};

fetch(url, options).then((response) => {
console.log(response.status);
});

其中最大的不同之处在于传递数据的方式不同,Axios是放到data属性里,以对象的方式进行传递,而Fetch则是需要放在body属性中,以字符串的方式进行传递。

4. 响应超时

Axios的相应超时设置是非常简单的,直接设置timeout属性就可以了,而Fetch设置起来就远比Axios麻烦,这也是很多人更喜欢Axios而不太喜欢Fetch的原因之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
axios({
method: "post",
url: "http://example.com/",
timeout: 4000, // 请求4秒无响应则会超时
data: {
firstName: "David",
lastName: "Pollock",
},
})
.then((response) => {
/* 处理响应 */
})
.catch((error) => console.error("请求超时"));

Fetch提供了AbortController属性,但是使用起来不像Axios那么简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const controller = new AbortController();

const options = {
method: "POST",
signal: controller.signal,
body: JSON.stringify({
firstName: "David",
lastName: "Pollock",
}),
};
const promise = fetch("http://example.com/", options);

// 如果4秒钟没有响应则超时
const timeoutId = setTimeout(() => controller.abort(), 4000);

promise
.then((response) => {
/* 处理响应 */
})
.catch((error) => console.error("请求超时"));

5. 对数据的转化

Axios还有非常好的一点就是会自动对数据进行转化,而Fetch则不同,它需要使用者进行手动转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// axios
axios.get("http://example.com/").then(
(response) => {
console.log(response.data);
},
(error) => {
console.log(error);
}
);

// fetch
fetch("http://example.com/")
.then((response) => response.json()) // 需要对响应数据进行转换
.then((data) => {
console.log(data);
})
.catch((error) => console.error(error));

Fetch提供的转化API有下面几种:

  • arrayBuffer()
  • blob()
  • json()
  • text()
  • formData()

使用Fetch时你需要清楚请求后的数据类型是什么,然后再用对应的方法将它进行转换。

Fetch可以通过一些封装实现Axios的自动转化功能,至于如何实现由于我没有去研究过所以就不再这里多嘴,不过实现起来应该不难,但是要将实现过程写的健壮就需要花费一定的时间。

6. HTTP拦截器

Axios的一大卖点就是它提供了拦截器,可以统一对请求或响应进行一些处理,相信如果看过一个比较完整的项目的请求封装的话,一定对Axios的拦截器有一定的了解,它是一个非常重要的功能。

使用它可以为请求附加token、为请求增加时间戳防止请求缓存,以及拦截响应,一旦状态码不符合预期则直接将响应消息通过弹框的形式展示在界面上,比如密码错误、服务器内部错误、表单验证不通过等问题。

1
2
3
4
5
6
7
8
9
axios.interceptors.request.use((config) => {
// 在请求之前对请求参数进行处理
return config;
});

// 发送GET请求
axios.get("http://example.com/").then((response) => {
console.log(response.data);
});

而Fetch没有拦截器功能,但是要实现该功能并不难,直接重写全局Fetch方法就可以办到。

1
2
3
4
5
6
7
8
9
10
11
12
fetch = ((originalFetch) => {
return (...arguments) => {
const result = originalFetch.apply(this, arguments);
return result.then(console.log("请求已发送"));
};
})(fetch);

fetch("http://example.com/")
.then((response) => response.json())
.then((data) => {
console.log(data);
});

7. 同时请求

同时请求在项目中用的不多,但是偶尔可能会用到。

Axios:

1
2
3
4
5
6
7
8
9
10
axios
.all([
axios.get("https://api.github.com/users/iliakan"),
axios.get("https://api.github.com/users/taylorotwell"),
])
.then(
axios.spread((obj1, obj2) => {
...
})
);

Fetch:

1
2
3
4
5
6
7
8
9
10
11
Promise.all([
fetch("https://api.github.com/users/iliakan"),
fetch("https://api.github.com/users/taylorotwell"),
])
.then(async ([res1, res2]) => {
const a = await res1.json();
const b = await res2.json();
})
.catch((error) => {
console.log(error);
});

8. 浏览器原生支持

Fetch唯一碾压Axios的一点就是现代浏览器的原生支持。

本着负责的态度(其实是因为这篇文章写得比较困难…因为我对Fetch的研究不深)在这几天,我多次尝试使用Fetch,习惯后觉得还挺好用的,最主要是浏览器原生就支持,不像Axios需要引入一个包,而且需要即时测试某些接口直接在Chrome浏览器中使用Fetch进行请求,尤其是编写爬虫或脚本的时候,你在当前网页打开Chrome的控制台使用Fetch几乎不需要什么配置就可以直接进行请求。

img

上图是在知乎打开Chrome控制台然后调用知乎个人数据API,可以看到能够成功的拿到数据。

9. 最后

Fetch可以实现所有Axios能够实现的功能,但是需要自行进行封装,如果不喜欢折腾直接在项目中使用Axios是一个非常明智的选择,这完全取决于你是否愿意使用浏览器内置API。

有时候新技术逐渐取代老技术是一个必然趋势,所以Fetch有一天终将会取代XMLHttpRequest,也许之后Axios库会改为使用Fetch请求

本文为转载文章 点击进入原文

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

海底掘金自动化挖矿实现

发表于 2021-11-29

前言

最近掘金出了一款小游戏叫做海底掘金, 挖取矿石可以兑换周边礼品

我眼馋那款游戏机已久, 于是乎就玩起了这款游戏, 不过每天手动操作实在太过劳神, 于是, 我打算将其流程自动化

所有的数据都是离不开接口, 我们只需要模拟接口请求即可

那么首先要做的就是分析游戏中每一个动作都发送了什么数据

动作分析

  1. 上移

    1
    {"command":["U"]}

    image-20211129115453501

  2. 下移

    1
    {"command":["D"]}

    image-20211129115518866

  3. 左移

    1
    {"command":["L"]}

    image-20211129115538071

  4. 右移

    1
    {"command":["R"]}

    image-20211129115556431

  5. 跳跃

    1
    2
    3
    4
    5
    6
    7
    8
    //上跳跃
    {command: ["8"]}
    //下跳跃
    {command: ["2"]}
    //左跳跃
    {command: ["4"]}
    //右跳跃
    {command: ["6"]}

    image-20211129120520331

  6. 一层循环2次上移

    1
    {command: [{times: 2, command: ["U"]}]}
    • times: 表示循环次数

    image-20211129115642187

  7. 两层循环

    1
    {command: [{times: 2, command: [{times: 3, command: ["D"]}]}]}

    image-20211129115708099

    多层循环则多层嵌套

接口分析

  1. 动作接口

    1
    https://juejin-game.bytedance.com/game/sea-gold/game/command?uid=xxx&time=xxx
  2. 游戏结束接口

    1
    https://juejin-game.bytedance.com/game/sea-gold/game/over?uid=xxx&time=xxx
  3. 游戏开始接口

    1
    https://juejin-game.bytedance.com/game/sea-gold/game/start?uid=xxx&time=xxx
  4. Token获取接口

    1
    https://juejin.cn/get/token

    这个接口获取到的值其实就是其他接口中请求头authorization的值, 具体往下看

  5. 获取矿石数量相关信息

    1
    https://juejin-game.bytedance.com/game/sea-gold/home/info?uid=xxx&time=xxx

浏览器控台自动化实现

我们跳过登录验证操作, 先从简单的控台自动化入手, 实现动作的自动化, 有效避免作弊检测

比如我们想要实现以下动作的反复执行

image-20211129121755381

那么发送的数据如下:

1
{"command":[{"times":10,"command":["L","2","R",{"times":10,"command":["L","4","D",{"times":10,"command":["R","D","2"]}]}]}]}

这一步我们可以直接从浏览器控制台里拷贝出来, 不用自己计算, 方便快捷准确

然后结合动作接口和请求头进行模拟请求即可

如下请求头

  • Content-type
  • authorization (关键)
  • x-tt-gameid (关键)
  • accept

以上请求头数据直接从控制台拷贝

代码实现如下:

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
await (async function autoRun(){
//以下三个字段值替换成你自己的就行
let uid = 'xxx';// 你的uid
let authorization = 'xxx'; // request headers中的authorization
let gameid = 'xxx'; // request headers中的x-tt-gameid


let params = {"command":[{"times":10,"command":["L","2","R",{"times":10,"command":["L","4","D",{"times":10,"command":["R","D","2"]}]}]}]};

let datarus = await fetch('https://juejin-game.bytedance.com/game/sea-gold/game/command?uid=' + uid + '&time=' + Date.parse(new Date()), {
method: 'POST',
credentials: "include",
headers: {
'Content-type': 'application/json; charset=UTF-8',
'authorization': authorization,
'accept': 'application/json, text/plain, */*',
'content-length': JSON.stringify(params).length,
'x-tt-gameid': gameid,
},
body: JSON.stringify(params)
}).then(async (res) => {
return res.json();
});

return datarus;

})();

注意, 每一局的gameId是不一样的, 使用相同的gameId会报游戏异常, 这种操作大家要尽量避免, 谁知道官方有没有利用这个来判定玩家是否作弊呢, 谨慎为上

请求成功后响应数据如下:

1
{"data":{"appendMapData":[0,0,133,0,0,0,27,122,6,0,0,22,6,0,0,6,135,6,0,21,21,0,0,122,0,0,0,0,23,0,6,6,0,0,6,0,25,0,0,121,0,6,6,0,29,0,6,6,0,151,6,0,0,6,27,0,0,6,0,0,0,0,125,6,0,0,0,5,0,135,113,143,0,6,131,0,0,102,101,151,6,6,0,0,0,0,3,0,0,6,0,6,6,6,122,0,6,6,0,6,6,0,0,0,29,0,6,0,6,0,0,101,6,0,24,6,6,0,0,0,6,0,6,6,6,132,0,6,0,0,0,0,6,0,5,6,6,131,114,0,0,0,0,0,0,6,6,0,6,0,6,6,6,0,104,0,6,0,4,0,0,0,6,125,6,113,0,0,0,6,0,6,6,0,0,0,6,6,0,0,6,0,5,6,5,121,28,0,0,24,0,0,6,151,0,0,141,6,0,24,0,6,0,0,6,6,0,0,131,6,105,6,0,0,27,113,6,0,6,0,0,143,0,0,101,0,29,0,0,6,0,0,0,133,6,6,25,24,151,29,6,104,6,0,6,6,5,0,0,131,6,131,6,6,0,0,0,6,0,28,133,0,124,6,6,0,0,0,131,0,22,0,0,0,0,0,0,0,22,131,6,6,0,6,6,24,124,6,0,0,101,6,6,0,0,0,103,0,6,26,0,0,0,0,103,0,6,0,6,131,0,6,0,6,6,6,0,141,6,29,0,0,0,6,0,3,6,0,6,6,6,0,0,114,0,0,6,0,0,6,0,6,0,6,6,6,0,0,28,23,6,102,0,115,4,6,6,0,6,27,6,6,0,6,6,0,0,24,0,3,6,0,0,143,0,29,0,26,0,4,6,105,0,0,0,6,0,21,132,21,0,0,0,6,4,103,6,0,27,6,0,24,24,0,6,104,0,104,0,0,6,6,152,6,0,6,0,0,0,0,0,0,0,6,24,6,6,23,143,0,0,24,0,6,0,6,0,0,0,151,0,26,0,6,0,0,0,0,6,0,0,0,0,6,135,0,6,6,0,0,0,0,29,6,0,0,6,0,24,25,6,0,0,0,0,0,6,0,0,6,0,6,152,28,6,25,6,0,6,0,6,6,0,0,0,25,0,0,6,122,6,0,0,6,112,6,0,0,124,0,0,0,0,25,0,6,6,6,6,27,112,24,121,6,6,0,6,0,6,0,6,115,0,22,6,6,6,115,6,6,101,0,27,5,105,0,0,0,0,0,0,0,6,0,0,0,3,6,6,0,0,0,0,6,0,0,0,0,26,6,6,0,0,6,6,6,6,6,0,6,0,6,6,0,0,0,0,6,0,6,6,0,0,3,5,104,0,0,122,27,0,0,0,0,23,0,0,0,0,0,4,23,25,6,0,153,0,0,0,0,0,6,0,0,0,6,6,6,0,6,0,4,0,6,6,0,3,0,112,0,0,0,0,0,0,6,0,113,0,0,0,6,6,21,0,6,124,0,6,115,0,0,23,132,0,0,6,0,6,6,0,6,6,6,0,6,0,6,0,0,6,6,0,6,6,26,0,134,0,6,6,6,0,0,0,0,0,0,0,0,0,6,6,6,113,111,0,6,0,6,0,0,0,0,0,124,0,0,6,0,22,6,6,24,22,6,104,0,125,0,0,6,6,0,6,6,0,0,121,6,0,6,0,0,6,0,6,6,0,0,0,0,0,25,0,0,6,0,6,6,6,111,0,0,6,0,5,0,0,0,6,6,6,0,6,0,123,6,0,6,25,0,0,6,113,0,0,6,0,0,0,0,0,6,0,0,6,122,6,134,0,0,6,0,134,0,6,0,0,0,6,103,0,24,0,6,0,6,0,0,0,0,0,6,6,0,132,6,0,6,103,6,0,6,28,26,6,0,111,6,131,0,27,27,0,6,6,0,102,0,0,6,0,6,0,0,6,21,6,0,6,0,6,121,26,0,6,0,6,0,0,0,0,0,0,6,0,24,0,0,0,6,0,4,0,22,23,28,0,6,6,111,153,131,0,0,0,0,0,6,27,0,0,23,0,0,0,0,6,0,0,104,0,0,0,104,6,0,0,0,6,6,0,0,6,29,6,0,0,0,0,6,6,0,0,6,6,0,0,0,23,6,0,0,0,6,0,113,6,0,6,6,6,0,0,0,29,152,0,0,0,153,0,6,6,0,6,6,0,26,6,6,6,151,21,0,21,6,0,21,6,6,6,0,6,0,0,0,22,6,6,6,0,6,0,21,6,6,0,6,6,22,0,5,0,6,0,0,0,0,0,28,6,0,6,114,0,6,0,0,6,0,23,0,0,6,0,0,114,6,6,0,0,0,6,0,0,0,0,6,0,6,6,0,0,0,0,0,0,0,24,0,0,0,6,134,0,0,0,131,0,0,0,0,6,6,25,0,0,6,5,0,104,0,6,6,0,0,6,0,0,0,6,0,6,101,0,0,6,0,6,0,0,0,125,0,0,6,134,0,6,6,27,0,6,0,131,6,0,0,6,0,0,6,0,123,0,0,0,0,6,0,0,6,6,6,6,0,6,0,6,0,0,0,25,0,0,0,0,0,6,0,0,6,0,6,0,0,0,6,0,6,6,6,114,6,6,0,0,0,0,0,6,0,0,6,124,6,6,0,0,154,6,0,152,0,0,22,6,6,6,0,6,26,0,0,111,0,133,29,0,0,6,6,104,133,0,0,6,26],"curPos":{"x":5,"y":210},"blockData":{"moveUp":29,"moveDown":30,"moveLeft":14,"moveRight":27,"jump":7,"loop":15},"gameDiamond":112},"code":0,"message":"success","logId":"xxxx","serviceTime":1638159487113}

注意看返回的gameDiamond这个字段, 我们执行一次接口立马就获取了112个矿石, 也就是说我们掉完接口立马结束游戏, 那么这矿石就到手了, 省去了游戏执行的时间

在同一局中, 我们可以反复调接口, 直到道具不足为止, 如果道具不足就重开一局再过一遍之前的代码

到这里 问题就来了, 每一局的x-tt-gameid都不一样, 每新开一局都要拷贝一次太繁琐了,接下来我们解决x-tt-gameid的问题

签名破解

接下来全局翻找源码, 从js源码中, 我大致猜测这个x-tt-gameid是由ES256算法生成accessToken, 找到如下代码:

1
h.default.sign({gameId:this.gameId,time:t},"-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIDB7KMVQd+eeKt7AwDMMUaT7DE3Sl0Mto3LEojnEkRiAoAoGCCqGSM49\nAwEHoUQDQgAEEkViJDU8lYJUenS6IxPlvFJtUCDNF0c/F/cX07KCweC4Q/nOKsoU\nnYJsb4O8lMqNXaI1j16OmXk9CkcQQXbzfg==\n-----END EC PRIVATE KEY-----\n",{algorithm:"ES256",expiresIn:2592e3,header:{alg:"ES256",typ:"JWT"}});return console.log("token",e)

当然也只是猜测, 需要进行验证

为了方便测试, 我将代码测试从控台转移到nodejs环境中

对于nodejs不太熟悉的朋友, 我这里简单介绍一下:

nodejs可以通俗理解为是模拟了一个浏览器的环境, 相当于java中的jre, 以前js代码只能在浏览器中执行, 有了nodejs环境, 我们就可以脱离浏览器随处运行js代码啦

Nodejs官网

好了 下载安装完nodejs之后, 安装jsonwebtoken模块用于jwt的生成

1
npm install jsonwebtoken

然后运行以下代码测试:

1
2
3
4
5
const jwt = require('jsonwebtoken');
const privatekey = "-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIDB7KMVQd+eeKt7AwDMMUaT7DE3Sl0Mto3LEojnEkRiAoAoGCCqGSM49\nAwEHoUQDQgAEEkViJDU8lYJUenS6IxPlvFJtUCDNF0c/F/cX07KCweC4Q/nOKsoU\nnYJsb4O8lMqNXaI1j16OmXk9CkcQQXbzfg==\n-----END EC PRIVATE KEY-----\n"

console.log(jwt.decode("eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJnYW1lSWQiOiIyMDIxLTExLTI2IDA5OjUwOjIwIiwidGltZSI6IjE2Mzc5MTMyNjIwMTkiLCJpYXQiOjE2Mzc5MTMyNjIsImV4cCI6MTY0MDUwNTI2Mn0.2SWFcrvSMGzRIhsZe6ny0ugj5eoJnb4DlGGudFzUgvwPTqbeZaSa2FG5jYF5VPOXfv7Ye0TlYPt3VmviclOGKQ")
)

解码得到:

1
2
3
4
5
6
{
gameId: '2021-11-26 09:50:20',
time: '1637913262019',
iat: 1637913262,
exp: 1640505262
}

至此, 我们既验证了密钥的正确性, 同时得到了加密参数, 而这个time则是当前时间毫秒值

那接下来我们需要在游戏中实际验证一下才行, 验证步骤如下:

  1. 调用游戏开始接口获取返回的gameId
  2. 将gameId进行ES256加密得到签名
  3. 调用动作接口, 并将签名放入x-tt-gameid请求头中
  4. 请求成功, 验证ok

好了 我们的第一阶段的破解工作已完毕, 接下来就是对代码进行整理, 实现自动化运行

由于代码转移到nodejs上之后使用fetch进行网络请求代码一直报错, 因此请求方式改为axios

这里多一嘴, 虽然nodejs模拟了浏览器的环境, 但是浏览器内置的一些API, nodejs上不一定也内置了, 要用的话的npm手动安装, 比如fetch是浏览器内置的API, 控台直接可以使用, 如果要在node上用的话可以执行以下指令安装:

1
npm install node-fetch

然后代码中引用:

1
const fetch = require('node-fetch');

同样axios也是:

1
npm install axios

代码引用:

1
const axios = require('axios');

关于fetch和axios两者的实现原理和区别, 有兴趣的可自行百度, 这里就不展开介绍了

回过头来, 整理后的代码如下:

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
const jwt = require('jsonwebtoken');
const axios = require('axios');


const uid = 'xxx';// 你的uid
let authorization = 'xxx'; // request headers中的authorization


let currentTime = Date.parse(new Date())
let gameId = "2021-11-30 10:36:12"
function getAccesssToken() {

const privatekey = "-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIDB7KMVQd+eeKt7AwDMMUaT7DE3Sl0Mto3LEojnEkRiAoAoGCCqGSM49\nAwEHoUQDQgAEEkViJDU8lYJUenS6IxPlvFJtUCDNF0c/F/cX07KCweC4Q/nOKsoU\nnYJsb4O8lMqNXaI1j16OmXk9CkcQQXbzfg==\n-----END EC PRIVATE KEY-----\n"
currentTime = Date.parse(new Date())
const payload = { "gameId": gameId, "time": currentTime }

const accessToken = jwt.sign(payload, privatekey, {
expiresIn: "240h",
algorithm: "ES256",
});
//console.log(accessToken)
return accessToken
}


//游戏开始
async function gameStart() {



//选择 click这个角色 钻石有加成
let params = { "roleId": 2 };

const options = {
url: 'https://juejin-game.bytedance.com/game/sea-gold/game/start?uid=' + uid + '&time=' + Date.parse(new Date()),
method: "POST",
credentials: "include",
headers: {
'Content-type': 'application/json; charset=UTF-8',
'authorization': authorization,
'accept': 'application/json, text/plain, */*',
'content-length': JSON.stringify(params).length,
'x-tt-gameid': '',
'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Mobile Safari/537.36'
},
data: params,
};

return await axios(options).then((response) => {
console.log(response.data);
if (response.data.code == 4007) {
//游戏正在执行 则结束游戏
// gameOver()

} else if (response.data.code == 0) {
//成功开始
gameId = response.data.data.gameId


}
});


}
//执行游戏动作
async function gameRun() {


let gameid = getAccesssToken(); // request headers中的x-tt-gameid



let paramsList = [{ "command": [{ "times": 10, "command": ["L", "2", "R", { "times": 10, "command": ["L", "4", "D", { "times": 10, "command": ["R", "D", "2"] }] }] }] },
{ "command": [{ "times": 10, "command": ["L", "D", "R", { "times": 10, "command": ["L", "4", "D", { "times": 10, "command": ["R", "D", "2"] }] }] }] },
{ "command": [{ "times": 10, "command": ["U", "4", "L", { "times": 10, "command": ["L", "4", "D", { "times": 10, "command": ["R", "D", "2"] }] }] }] }
];

let params = paramsList[Math.floor(Math.random() * paramsList.length)];

const options = {
url: 'https://juejin-game.bytedance.com/game/sea-gold/game/command?uid=' + uid + '&time=' + currentTime,
method: "POST",
credentials: "include",
headers: {
'Content-type': 'application/json; charset=UTF-8',
'authorization': authorization,
'accept': 'application/json, text/plain, */*',
'content-length': JSON.stringify(params).length,
'x-tt-gameid': gameid,
},
data: params,
};

return await axios(options).then((response) => {
console.log(response.data);

if (response.data.code == 0) {
//成功

return response.data.data.gameDiamond

} else {
//4009 代码块不足
return 0
}
});

}
//结束游戏
async function gameOver() {



let params = { "isButton": 1 };

const options = {
url: 'https://juejin-game.bytedance.com/game/sea-gold/game/over?uid=' + uid + '&time=' + Date.parse(new Date()),
method: "POST",
credentials: "include",
headers: {
'Content-type': 'application/json; charset=UTF-8',
'authorization': authorization,
'accept': 'application/json, text/plain, */*',
'content-length': JSON.stringify(params).length,
},
data: params,
};

return await axios(options).then((response) => {
console.log(response.data);
if (response.data.code == 0) {
//成功

return response.data.data.todayDiamond>=response.data.data.todayLimitDiamond

} else {

return true
}
});


}
async function delay(time) {

var start = Number(new Date());
while (start + time * 200 > Number(new Date())) { }
}
async function start() {
//开始游戏-->游戏执行-->游戏结束
for (let index = 0; index < 100; index++) {
console.log("倒计时:" + index)
await gameStart()
let time = await gameRun()


//同步延迟结束游戏 根据获取的金币而定 金币越多 延迟时间越长
await delay(time)
let info=await gameOver()
//金币达到上限 停止游戏
if(info){
return
}
}


}

start()

将以上代码拷贝到你的本地文件中, 填入你的游戏authorization和uid, 然后每天用node运行一遍即可

到这里, 第一阶段的自动化已然实现

程序定时执行

每天手动跑代码太费事, 稍不留神就有可能某天给漏了, 尤其是周六日不开电脑的时候岂不是没法执行脚本了

这时 我们可以考虑放到服务器中每天定时执行, 365天风雨无阻, 推荐大家使用github或者travis ci免费服务部署

我这里手上刚好有一台阿里云的服务器, 因此直接用服务器跑, 这里我使用的是crontab定时程序, 关于crontab的用法, 可参见《Linux Crontab 命令安装和使用教程:在 VPS 上设置定时任务》

以ubuntu为例具体操作步骤如下:

  1. 将本地脚本拷贝至服务器

    1
    scp 脚本文件 root@服务器IP:/root

    注意:冒号前后不能有空格, 否则会提示目录不存在

  2. 登录服务器配置crontab任务

    执行crontab -e进入cron编辑界面, 将以下定时代码写入

    1
    0 8 * * *  /usr/bin/node /root/jwt_test >>/root//log/jwt_test_$(date +\%Y-\%m-\%d-\%H).log 2>&

    表示每天8点执行该脚本

  3. 安装nodejs及相关依赖

    如果服务器已经安装nodejs可忽略该步操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //更新源
    sudo apt-get update
    //安装nodejs
    sudo apt-get install -y nodejs
    //安装npm包管理器
    sudo apt-get install npm
    //安装脚本依赖
    npm install jsonwebtoken
    npm install axios
  4. 配置完毕

本文为作者原创转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

android中获取控件宽高的几种方式总结

发表于 2021-11-26

前言

我们知道, 在Activity的onCreate中通过width和height方法是通常是无法获取到控件的宽高的

因为控件还未测量完毕

如果想要获取到值, 那么有以下几种方式可选

第一种 获取前调用一次measure方法

示例如下:

1
2
3
4
5
6
7
int width = View.MeasureSpec.makeMeasureSpec(0,
View.MeasureSpec.UNSPECIFIED);
int height = View.MeasureSpec.makeMeasureSpec(0,
View.MeasureSpec.UNSPECIFIED);
view.measure(width, height);
view.getMeasuredWidth(); // 获取宽度
view.getMeasuredHeight(); // 获取高度

第二种 使用ViewTreeObserver. OnPreDrawListener进行绘制前事件监听

示例如下:

1
2
3
4
5
6
7
8
9
10
11
view.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {

@Override
public boolean onPreDraw() {
view.getViewTreeObserver().removeOnPreDrawListener(this);
view.getWidth(); // 获取宽度
view.getHeight(); // 获取高度
return true;
}
});

该方法会被调用多次,因此获取到视图的宽度和高度后要移除该监听事件

第三种 使用ViewTreeObserver. OnGlobalLayoutListener

在布局发生改变或者某个视图的可视状态发生改变时调用该事件

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
view.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {

@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT >= 16) {
view.getViewTreeObserver()
.removeOnGlobalLayoutListener(this);
}
else {
view.getViewTreeObserver()
.removeGlobalOnLayoutListener(this);
}
view.getWidth(); // 获取宽度
view.getHeight(); // 获取高度
}
});

该方法会被多次调用,因此需要在获取到视图的宽度和高度后执行 remove 方法移除该监听事件。

第四种 使用View.OnLayoutChangeListener

在视图的 layout 改变时调用该事件, API>=11才能使用

示例如下:

1
2
3
4
5
6
7
8
9
10
11
view.addOnLayoutChangeListener(
new View.OnLayoutChangeListener() {

@Override
public void onLayoutChange(View v, int l, int t, int r, int b,
int oldL, int oldT, int oldR, int oldB) {
view.removeOnLayoutChangeListener(this);
view.getWidth(); // 获取宽度
view.getHeight(); // 获取高度
}
});

该方法同样会被多次调用,因此需要在获取到视图的宽度和高度后执行 remove 方法移除该监听事件。

第五种 View.post() 方法

方法会在 View 的 measure、layout 等事件完成后触发

示例如下:

1
2
3
4
5
6
7
8
view.post(new Runnable() {

@Override
public void run() {
view.getWidth(); // 获取宽度
view.getHeight(); // 获取高度
}
});

该方法只会执行一次,且逻辑简单,建议使用

第六种 重写 View 的 onSizeChanged 方法

在视图的大小发生改变时调用该方法,会被多次调用,因此获取到宽度和高度后需要考虑禁用掉代码

示例如下:

1
2
3
4
5
6
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);

view.getWidth(); // 获取宽度
view.getHeight(); // 获取高度

该实现方法需要继承 View,且多次被调用,不建议使用

第七种 重写 View 的 onLayout 方法

示例如下:

1
2
3
4
5
6
7
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);

view.getWidth(); // 获取宽度
view.getHeight(); // 获取高度
}

该实现方法需要继承 View,且多次被调用,不建议使用

实际场景

目前有个业务需求, 我需要在addView之后获取到被add子控件的宽高, 此时最好的方法就是使用View.post, 如下:

image-20211126123723329

本文为作者原创转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

android中获取控件坐标的几种api区分

发表于 2021-11-25

前言

我们经常会看到以下几个方法:

  • getGlobalVisibleRect()

  • getLocalVisibleRect()

  • getLocationOnScreen()

  • getLocationInWindow()

傻傻分不清? 今天我们简单介绍一下它们各自的作用

getGlobalVisibleRec

方法作用:

其一: 用于判断控件是否可见, 以屏幕为参考 如果可见返回true, 不可见返回false, 我们可以借助该方法检测控件是否被滑出屏幕外

其二: 用于获取控件可见区域的坐标, 这个坐标是相对于屏幕而言

在这里插入图片描述

坐标存放在了Rect对象中, 以参数的形式传入:

1
getGlobalVisibleRect(new Rect)

getLocalVisibleRect

这个方法和上面的getGlobalVisibleRec方法类似, 唯一区别是参考系不同, 这个方法参考系是控件本身

以下是这两个方法的坐标打印:

getRect

getLocationOnScreen

获取控件相对于屏幕而言的坐标位置, 与可不可见无关

getLocationInWindow

获取控件相对于父控件而言的坐标位置, 与可不可见无关

在这里插入图片描述

坐标补充

我们在进行自定义控件绘制的时候, 总是容易被一些参数给绕的头晕脑涨, 什么top right, left, bottom, 四条边与屏幕的距离等等, 想想就觉得费劲, 再加上Android中坐标系和我们数学中的坐标系不一致 使得我们在进行计算的时候难免有点吃不消

实际上对于一些规则的图形绘制, 比如矩形, 我们需要明白的一点, 那就是只需要两个点的坐标即可绘制出来, 也就是两个对角点

那我们在进行图形绘制的时候, 将计算范围缩小, 焦点集中在两个对角坐标位置计算上即可

这两个对角可以使左上角右下角, 也可以是左下角和右上角

这样一想, 那么所谓的left和top实际就是左上角的xy坐标, 而right和 bottom就是右下角的坐标

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

关于Android中adjustResize属性不生效的问题

发表于 2021-11-22

前言

写聊天布局的时候需要给Activity用上adjustResize属性来保证输入框不会被软键盘遮盖住

如果该属性不生效, 那么一般情况是没有给布局添加fitsSystemWindows属性, 那么给布局根控件添加该属性即可:

1
android:fitsSystemWindows="true"

此时adjustResize属性生效, 但是新问题又出现了, 布局上方出现了一块间隔区域

image-20211122135418218

解决方案

重写根控件的onApplyWindowInsets方法, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class ChatLayout @JvmOverloads constructor(

context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {

override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
return super.onApplyWindowInsets(insets.replaceSystemWindowInsets(0, 0, 0,
insets.systemWindowInsetBottom
));
}
}

问题解决

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

关于继承和接口的形象理解

发表于 2021-11-19

前言

很多Java初学者对于继承和接口的作用区分总是很模糊, 如果不是因为回调需要接口, 估计能用继承实现的都用了继承

继承和接口有很多相似的地方, 比如抽象实现

但是它们的不同点呢 这个就得考虑到实际应用场景了, 脱离了业务研究 啥也不是

今天我们只说一个形象比喻

继承是先天 接口是后天

何为先天, 一生下来就有, 不要也得要, 说的高端一点这叫天赋

何为后天, 和父母无关 全靠自己

比如 父母有个好嗓子, 你继承了这个优点, 不用努力就比别人强

随着年龄的增长, 你发现干IT能促进植发行业的发展, 于是乎通过后天努力掌握了编程技能

那么

好嗓子–继承而来–继承

编程技能–后天获取–接口

似乎还是有点抽象 哈哈, 具体一点, 比如我们想让所有的子类都有相同的一个功能, 那我们可以使用继承

如果我们想让其中一部分子类有某个功能, 而剩下一些没有, 则可以使用接口, 实现差异化处理

也就是说接口的功能 可要可不要, 想要什么样的就接什么样的 比较自由 而继承的话全部子类必须都得有 没得选

后天可以弥补先天的不足, 接口可以弥补继承的不全面

不知道能不能理解

无妨 , 编程这东西 就是编程来的 慢慢就都明白了, 以上也只是个人的小小想法罢了

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

1…181920…48

乱码三千

android程序员一枚,擅长java,kotlin,python,金融投资,欢迎交流~

479 日志
139 标签
RSS
© 2024 乱码三千
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%