空空

空空 查看完整档案

金华编辑  |  填写毕业院校  |  填写所在公司/组织 mrone.top 编辑
编辑

写BUG工程师

个人动态

空空 收藏了文章 · 6月23日

Python 从零开始爬虫(九)——模拟登录,cookie的使用

某些网站,登录和没登录,用户的权限是不一样的,帐号登录之后才能获取更多的信息。更有甚者一上来就是登录界面,不登录就不给你进去(如p站)。爬取目标不用登录固然是好,但需要时也没办法啊,这时如果还想爬取信息,就必须让爬虫学会登录。

Cookie

说到这里就要介绍一下本文的小主角cookie了,简单的说,cookie是服务器安在客户端的“监视器”,记录了包括登录状态在内的所有信息,这些信息由服务器生成和解释,服务器通过客户端携带的cookie来识别用户。cookie存在生命周期,短的关掉浏览器就失效,长的能若干天免登陆,一旦失效就要重新获取。所以只要得到登录后的cookie并必要时进行更新,服务器就会认定其为登录状态。本文将介绍几种主流方法来模拟登录

注意:并不是说学会这几种方法你就完全掌握了模拟登录,加密,验证系统也可能成为模拟登录的头号劲敌,如果查遍全网都没得到满意的答案,selenium大佬也被识别了,那估计是没救了

从浏览器获取

这是最简单也是最容易见效的方法,在浏览器上登录并进行足够多操作后获得便能得到足量的cookie,打开F12捉包观其headers即可
图片描述

图中上方的set-cookie是响应cookie,也就是服务器要保存在客户端的cookie;下方的cookie则是要提交给服务器的cookie,也是我们的目标,让requests使用这个cookie有两种方法,一是原封不动把cookie字符串放入headers字典中提交,二是把cookie字符串变成字典再由cookies参数提交,转换方法如下。

def cookie_to_dict(cookie):
    cookie_dict = {}
    items = cookie.split(';')
    for item in items:
        key = item.split('=')[0].replace(' ', '')
        value = item.split('=')[1]
        cookie_dict[key] = value
    return cookie_dict

这种直接获取的方法缺点也很明显,就是不能追踪set-cookie并更新,原来的cookie一旦失效,就要从新手动获取

session维持

session名为“会话”,即多个请求的行为。requests库提供了会话对象(requests.Session)让使用者能跨请求保持某些参数如cookie,而且能自动处理服务器发来的cookie,使得同一个会话中的请求都带上最新的cookie,非常适合模拟登录。使用上也非常简单,实例化后几乎相当于一个requests对象拥有get,post等方法,text,cotent等属性。为了方便下次登录,还可以把第一次session登录后的cookie通过cookiejar保存到本地供下次读取免去登录

import requests
import http.cookiejar as cj
r = requests.session()
r.cookies = cj.LWPCookieJar()  # 接入容器
r.get(url,headers=,cookie=) # 不过需要注意,就算使用了会话,方法级别的参数也不会被跨请求保持,此cookie只发送给该请求
print(r.text)
r.post(url,headers=,data=,)
#请求x N
r.cookies.save(filename='cookies.txt', ignore_discard=True, ignore_expires=True)  # 保存cookie到本地,忽略关闭浏览器丢失,忽略失效
r.close() # 对话支持with打开以实现自动close


'''#载入本地cookie
s = requests.session()
s.cookies = cj.LWPCookieJar(filename='cookies.txt')
s.cookies.load(filename='cookies.txt', ignore_discard=True)
'''

虽说session使cookie管理变得一劳永逸,但登录包中post参数的构造可能是一个深坑,post的是帐号密码明文那简单;蛋疼的是如果帐号密码连同其他数据经js加密成密文作为post数据,那你就得从js中挑选并分析加密算法,还要用python实现(某些加了混淆的js像天书一样)。如果无法破译加密,requests登录就是一张白纸,cookie就更不用谈了,给你再牛逼的管理工具也没用。同时对方程序员的勤奋程度也是一个考虑因素,别人经常改算法,你也要从新看js改代码。

-----------------------------看看人家知乎,加密到连名字都没有了,js还混淆,如何下手?-------------------------------
图片描述

综上,session适用于没有加密的登录或者加密算法比较简单并且不常更新的网站。遇上无解的加密算法?要么手操拷贝cookie,要么请selenium大佬出场。

selenium大法

大佬虽然是慢了点,但永远是你大佬。借助浏览器完备的js解析能力,你根本不用考虑它是如何加密的,只要输入账号密码,最多再加个验证码,浏览器直接运行js把他们直接变成密文并post过去,完成登录,就像我们平时操作那么简单。所以有一种巧妙的方法是先用selenium进行模拟登录,然后再提取cookie给session用免去session模拟登录的过程(当然也可以继续selenium下去)。在代码中,往往就是定位,点击,定位,发送帐号密码,Enter(或者定位点击登录键),等待一段时间让cookie加载完后将其打包成RequestsCookieJar给session用就可以了。如对付知乎可以这样做:

import requests
from selenium import webdriver
import time
from selenium.webdriver.common.keys import Keys

driver = webdriver.Chrome()
driver.maximize_window()
driver.get('https://www.zhihu.com/')
# 不考虑验证码的情况
driver.find_element_by_xpath('//button[@data-za-detail-view-id="2278"]').click() #点击登录进入登录界面
driver.find_element_by_xpath('//input[@name="username"]').send_keys('account') #发送帐号名
driver.find_element_by_xpath('//input[@name="password"]').send_keys('password',Keys.ENTER) #发送密码并回车
time.sleep(10) # 等待cookie加载完成
cookies = driver.get_cookies()
print(cookies)

s=requests.Session()
c = requests.cookies.RequestsCookieJar()
for item in cookies:
    c.set(item["name"],item["value"])
print(c)
s.cookies.update(c) # 载入cookie
#s.get()
#s.post()

'''
'''

但就算是大佬,也不是万能的,有些网站能识别人操作的浏览器和selenium操作的浏览器,使登录受到拒绝,譬如用selenium模拟登录网易云音乐就会提示登录异常。如果不能对js进行逆向工程分析出其识别算法,那只能放弃selenium走requests那条要分析加密的老路

后记

到系列第十篇文章,爬虫系列也将接近尾声,最后的内容是多线程,多进程及协程的介绍及使用。至于scrapy应该就不会讲了,半年前刚入爬虫坑学的,学得浅,现在忘得差不多了,如今又被深度学习带跑了,但不排除突然复活的可能。心急的小伙伴可以去观摩大佬们的代码自学。本人就去挖DeepLearning的坑了。

查看原文

空空 关注了专栏 · 6月23日

Python 从零开始爬虫

基于python3.X,主要使用requests进行自由定制,适合兴趣向的爬虫教学

关注 60

空空 收藏了问题 · 4月12日

在B/S结构的系统中如何实现禁止一个账号在多处同时登录 ?

如题。为了防止用户进行账号分享使用,即需要禁止用户一个账号在多台机同时登录使用,注意,客户端为浏览器。应该如何设计这样一样一个方案?当出现重复登录时,需要把最先登录的那个T下线。如何来设计 ?假设系统是有一定的负载,需要考虑方案实现性能。
请大神们多考虑下你想到的方案有没有漏洞,感觉好难实现的。

--------后面补充------
如果您的方案是基于下面的思路实现的,在一个大型系统中不可行的:
1、靠存储用户登录状态信息,用户登录时判断该用户是否已有登录状态保存,这种是不可靠的,用户非正常退出后,他的登录状态信息会一值保持 ,需要做心跳检查。这个心跳时间是个问题。所以不可靠。
2、使用token存储在客户端实现的,只要token被用户分享,就可以共享账号登录。

空空 赞了回答 · 2018-11-03

解决关于《redis实战》中6-9代码片段使用setex锁的不解

请仔细查看一下手册,setnx命令是如果当前redis中没有设置的key才会设置成功返回1,否则设置失败返回0,把它当成锁使用,就是利用了这个特性,至于设置什么值关系不大。

关注 2 回答 2

空空 赞了回答 · 2018-09-11

解决yii hasOne 进行关联查询,查询指定的关联表的字段

with 关联的时候采用匿名函数查询
$query = self::find()

        ->select($fields)
        ->with(['building' => function ($query) {
            $query->select(['id']);
        }])

关注 3 回答 2

空空 发布了文章 · 2018-08-14

Yii2开发技巧 使用类似闭包的方式封装事务

在控制器中执行事务的时候,一般的代码如下:

$transaction = Yii::$app->db->beginTransaction();
try {
    //一些业务代码
    $transaction->commit();
} catch (\Exception $e) {
    $transaction->rollBack();
    throw $e;
}

于是我在想,这个代码结构,只有//一些业务代码 这一部分是不一样,却要重复很多遍,这一不是很冗余吗? 而且 不!好!看!,于是我试着寻找解决方法,一开始在stackflow找到一个类似的提问,有方案是在model里做封装,但是这样做有一定问题,如产生嵌套事务等,有兴趣的可以点击这里查看该问答

我们的Yii框架给出了一个方法transaction,乍一看好像不能解决传参的问题,我们先不管,往下看,该方法调用方式如下:

Yii::$app->db->transaction(function() {
    //一些业务代码
});

我们来看一下这个方法的源码

/**
 * Executes callback provided in a transaction.
 *
 * @param callable $callback a valid PHP callback that performs the job. Accepts connection instance as parameter.
 * @param string|null $isolationLevel The isolation level to use for this transaction.
 * See [[Transaction::begin()]] for details.
 * @throws \Exception|\Throwable if there is any exception during query. In this case the transaction will be rolled back.
 * @return mixed result of callback function
 */
public function transaction(callable $callback, $isolationLevel = null)
{
    $transaction = $this->beginTransaction($isolationLevel);
    $level = $transaction->level;

    try {
        $result = call_user_func($callback, $this);
        if ($transaction->isActive && $transaction->level === $level) {
            $transaction->commit();
        }
    } catch (\Exception $e) {
        $this->rollbackTransactionOnLevel($transaction, $level);
        throw $e;
    } catch (\Throwable $e) {
        $this->rollbackTransactionOnLevel($transaction, $level);
        throw $e;
    }

    return $result;
}

这个方法接受一个回调函数和事务的隔离级别,
从这里我们看出,这个方法虽然解决重复代码,却还有几个问题没有解决:
第一,这个方法抛出的异常我们需要在接收外面处理,我们不可能直接抛出,这样对客户端很不友好。
第二:没有记录日志的行为,即使出了问题也不容易排除。
第三:其实还是第一个问题,如果我们需要对每个异常做处理,在transaction方法外再嵌套一层try...catch...,那么和没有封装好像没什么区别?

根据方法可扩展不可修改的原则,我们应该在自己公共方法里对这个方法进行重载,重载代码如下:

public static function TransactionExecute(callable $function,$level=null)
{
    try{
        \Yii::$app->db->transaction($function,$level);
}catch (\Exception $e){
        //记录日志
        \Yii::error($e->getMessage());
        //这里可以理解成抛出自定义的异常类。
        (new self())->returnWayTip(1004, 'trans异常错误');
    }
}

然后回到如何传参的问题,我们可以使用闭包,贴一段伪代码,如下:

//执行事务
PublicFunction::TransactionExecute(function () use ($token_reward, $reward_info) {
        //业务代码
        $token_reward->save(0);
    MsgHelper::send($reward_info['post_id'], MsgHelper::SOMEONE_FINISH_REWARD, $reward_info);

    });




大功告成,代码看起来有没有更好看呢?

如有问题,欢迎指教。

查看原文

赞 0 收藏 0 评论 0

空空 发布了文章 · 2018-07-30

正确注释@return让PHPstorm动态返回类

场景是这样的,有一个BaseModel(继承自ActionRecord),所有的其他model都继承自它,然后其中有一个方法,简单贴下这个类的代码,:

class BaseModel extends ActiveRecord
{
    protected $temp_model;
    
    public function getCacheModel()
    {
        return $this->temp_model;
    }
}

这个方法的作用是取得在做参数验证时,从数据库查出的,缓存下来的实例对象。
这个时候,问题来了,我在取出这个对象的时候,PHPstorm没有了提示(如方法提示,属性提示等),按照一般的情况,只需要在方法前面加上@return注释就可以了。

/**
 * @return static
 */
public function getCacheModel()
{
    return $this->temp_model;
}

我们继续深入研究一下,关于这个static的意思,我特地在PHPDoc上查阅了一下,

static
An object of the class where this value was consumed, if inherited it will represent the child class. (see late static binding in the PHP manual).

Google翻译一下,大意如下:
消耗此值的类的对象,如果继承它将表示子类。
(参见PHP手册中的后期静态绑定)。

大概意思就是就会返回调用这个方法的类,如果是父类方法子类调用,那么将返回子类。

类似的还有2个

self
An object of the class where this type was used, if inherited it will still represent the class where it was originally defined.
$this
This exact object instance, usually used to denote a fluent interface.

直译如下,
self:使用此类型的类的对象,如果继承它,它仍将表示最初定义它的类。
大意就是和static差不多,但是父类方法子类调用,仍然返回父类。
$this:这个确切的对象实例,通常用于表示流畅的界面。
和self差不多。

但是到了这里,我的问题仍然没有解决,无论我@return的值改成什么,仍然返回的是BaseModel,尽管我在这个getCacheModel()方法里打印 self::className() 时,出现的是子类名。

于是我们继续往上面看,我是在controller调用的,controller的代码如下:

public function actionCommitReward()
{
    $model=$this->goCheck(new TakeRewards(['scenario'=>'commit_reward']));

    //获取实际要修改的数据
    $reward = $model->getCacheModel();
}

看起来没有什么问题,这个时候我们要注意了, $model 是由$this->goCheck()调用得到的,我们去看一下goCheck方法:

//验证参数是否合法
public function goCheck($model, $dada = '')
{
    $data = $this->postData;//post传入的数据
    if ($model->load($data, '') && $model->validate())//数据效验

return $model;

    else (new PublicFunction())->returnWayTip('1001', PublicFunction::getModelError($model));//这里理解成抛异常
}

这里不规范的地方出现了,由于这里传入的是model(对象类型),所以PHPstorm并没法知道我们具体传入的是什么类,加上注释后:

/**
 * @param object $model
 * @param string $dada
 * @return model1|model2
 */

这样后,问题“勉强解决”。只是每增加一个表,会需要在@return里增加表相对应的类名,而且会有类本来不应该存在的属性被提示。

为什么这里不能用static呢? 因为这里是$this调用的,返回controller类,并没有什么用,而这个也导致了后面使用$model->getCacheModel()方法时,没有办法正常识别应该返回的类(返回什么类取决与goCheck的@return注释是什么)。

当然你可以不写注释,那么你会发现,所以的提示都没有了。
这次我才真正意识到了注释的重要性。。。原来PHPstorm之所以都提示,都是因为大家按PHPDoc的规范写了注释啊!

最后可能有同学会问了,为什么不把goChekc方法放到BaseModel里呢?对的,实际上规范的做法是应该这样的,但是因为我这样把Yii::$app->request->post()赋值在controller里的$this->postData里(虽然这样方便一丢丢),而且在做token换id的一些操作了进行了手动赋值,所以没有办法,因为在model获取不到这个postData,当然你一定要挪进去也是可以的,只不过每次都需要传参$this->postData,见仁见智吧。

但是,这2个方法都并不规范,$this->postData = Yii::$app->request->post(); 把全局的变量变成了一个局部变量,规范的做法应该是使用Yii::$app->request->post($name,$dafaultValue)来给post数据赋值。

最后,因为并不是我一个人在写,所以没有办法进行大刀阔斧地改动,只能尽可能地优化。BTW,希望大佬们有更好的意见可以说说,因为个人比较经验也比较不足,都是自己摸索。

查看原文

赞 0 收藏 0 评论 0

空空 发布了文章 · 2018-07-28

正确注释@return让PHPstorm动态返回类

场景是这样的,有一个BaseModel(继承自ActionRecord),所有的其他model都继承自它,然后其中有一个方法,简单贴下这个类的代码,:

class BaseModel extends ActiveRecord
{
    protected $temp_model;
    
    public function getCacheModel()
    {
        return $this->temp_model;
    }
}

这个方法的作用是取得在做参数验证时,从数据库查出的,缓存下来的实例对象。
这个时候,问题来了,我在取出这个对象的时候,PHPstorm没有了提示(如方法提示,属性提示等),按照一般的情况,只需要在方法前面加上@return注释就可以了。

/**
 * @return static
 */
public function getCacheModel()
{
    return $this->temp_model;
}

我们继续深入研究一下,关于这个static的意思,我特地在PHPDoc上查阅了一下,

static
An object of the class where this value was consumed, if inherited it will represent the child class. (see late static binding in the PHP manual).

Google翻译一下,大意如下:
消耗此值的类的对象,如果继承它将表示子类。
(参见PHP手册中的后期静态绑定)。

大概意思就是就会返回调用这个方法的类,如果是父类方法子类调用,那么将返回子类。

类似的还有2个

self
An object of the class where this type was used, if inherited it will still represent the class where it was originally defined.
$this
This exact object instance, usually used to denote a fluent interface.

直译如下,
self:使用此类型的类的对象,如果继承它,它仍将表示最初定义它的类。
大意就是和static差不多,但是父类方法子类调用,仍然返回父类。
$this:这个确切的对象实例,通常用于表示流畅的界面。
和self差不多。

但是到了这里,我的问题仍然没有解决,无论我@return的值改成什么,仍然返回的是BaseModel,尽管我在这个getCacheModel()方法里打印 self::className() 时,出现的是子类名。

于是我们继续往上面看,我是在controller调用的,controller的代码如下:

public function actionCommitReward()
{
    $model=$this->goCheck(new TakeRewards(['scenario'=>'commit_reward']));

    //获取实际要修改的数据
    $reward = $model->getCacheModel();
}

看起来没有什么问题,这个时候我们要注意了, $model 是由$this->goCheck()调用得到的,我们去看一下goCheck方法:

//验证参数是否合法
public function goCheck($model, $dada = '')
{
    $data = $this->postData;//post传入的数据
    if ($model->load($data, '') && $model->validate())//数据效验

return $model;

    else (new PublicFunction())->returnWayTip('1001', PublicFunction::getModelError($model));//这里理解成抛异常
}

这里不规范的地方出现了,由于这里传入的是model(对象类型),所以PHPstorm并没法知道我们具体传入的是什么类,加上注释后:

/**
 * @param object $model
 * @param string $dada
 * @return model1|model2
 */

这样后,问题“勉强解决”。只是每增加一个表,会需要在@return里增加表相对应的类名,而且会有类本来不应该存在的属性被提示。

为什么这里不能用static呢? 因为这里是$this调用的,返回controller类,并没有什么用,而这个也导致了后面使用$model->getCacheModel()方法时,没有办法正常识别应该返回的类(返回什么类取决与goCheck的@return注释是什么)。

当然你可以不写注释,那么你会发现,所以的提示都没有了。
这次我才真正意识到了注释的重要性。。。原来PHPstorm之所以都提示,都是因为大家按PHPDoc的规范写了注释啊!

最后可能有同学会问了,为什么不把goChekc方法放到BaseModel里呢?对的,实际上规范的做法是应该这样的,但是因为我这样把Yii::$app->request->post()赋值在controller里的$this->postData里(虽然这样方便一丢丢),而且在做token换id的一些操作了进行了手动赋值,所以没有办法,因为在model获取不到这个postData,当然你一定要挪进去也是可以的,只不过每次都需要传参$this->postData,见仁见智吧。

但是,这2个方法都并不规范,$this->postData = Yii::$app->request->post(); 把全局的变量变成了一个局部变量,规范的做法应该是使用Yii::$app->request->post($name,$dafaultValue)来给post数据赋值。

最后,因为并不是我一个人在写,所以没有办法进行大刀阔斧地改动,只能尽可能地优化。BTW,希望大佬们有更好的意见可以说说,因为个人比较经验也比较不足,都是自己摸索。

查看原文

赞 0 收藏 0 评论 0

空空 赞了文章 · 2018-07-26

一道看似简单的面试题

clipboard.png

前言

使用PHP,给定一个数,判断这个数是否是二的N次方

这样看似简单的一个面试题, 实际牵出了很多基础知识,本章在为大家补习基础知识的情况下来解答这道题。先亮出答案

function exponentiation($number)
{
    if ($number < 0) {
        return false;
    }
        
    if (($number & ($number - 1)) == 0) {
        return true;
    } else {
        return false;
    }
}
    
exponentiation (2);
exponentiation (8);
exponentiation (16);
exponentiation (32);
exponentiation (128);

这是查阅后的最最标准的答案,其他类似通过循环等等答案就略过把。

运算

实际这道面试题考的并非是算法,而是作为开发人员的你“底子”够不够。
运算符应该是每门语言hello world 后立马要学习的。运算符分很多种,赋值运算符,比较运算符等等。我想大多自学或者培训出道的没有经过系统化学习的童鞋的,对这块的知识应该很薄弱把。当然赋值、比较什么的很熟悉,因业务基本逃不过这些。但对位运算符,你真的吃透了吗?

这道题的考点一是位运算符的使用,上面说过了本题考察的并非算法,而是你对二进制的了解,而在php中能操作二进制的运算符貌似 (我的知识范围内) 只有位运算符

PHP位运算符 (部分)

本道面试题用到了 & 所以不详解其他的运算符,需要更多了解请移步官方
http://php.net/manual/zh/lang...

运算符附加信息
&按位与运算符
^按位异或运算符

按位与

按位与以简单易懂的方式来讲就是二进制位不相同的抵消,相同的保留

clipboard.png

举几个栗子

公式 -> 转为二进制后的公式 -> 二进制结果 -> 十进制结果
2 & 3 -> 0010 & 0011 -> 0010 = 2
10 & 7 -> 1010 & 0111 -> 0010 = 2
32 & 70 -> 0100000 & 1000110 -> 0000000 = 0

按位异或

按位异或以简单易懂的方式来讲就是二进制位相同的抵消,不相同的保留

clipboard.png

举几个栗子

公式 -> 转为二进制后的公式 -> 二进制结果 -> 十进制结果
2 ^ 3 -> 0010 ^ 0011 -> 0001 = 1
10 ^ 7 -> 1010 ^ 0111 -> 1101 = 13
32 ^ 70 -> 0100000 ^ 1000110 -> 1100110 = 102

进制

借用百度百科上的一段话

二进制是计算技术中广泛采用的一种数制。二进制数据是用0和1两个数码来表示的数。它的基数为2,进位规则是“逢二进一”,借位规则是“借一当二”。

二进制本身就是为2这个数字而使用的,所以说这道面试题直指二进制的使用是没错的。2的n次方则就是

n公式结果
122
22x24
32x2x28
42x2x2x216

换算成二进制的表格是

n公式结果
12000010
22x2000100
32x2x2001000
42x2x2x2010000

由此看出2的n次方的二进制最高位是1,其余补0,(n&(n-1))==0 并且 n> 0 的情况下必定是2的n次方,为什么要-1呢?在二进制中每一位必须都不相同&后才会得出0,上述已经讲解了&的运算结果。例如十进制的16

16 & (16 - 1) = 
010000 & (001111) = 0 

不要纠结(n&(n-1))==0 这个公式是怎么来的,作为程序员,我感觉应该把更多时间放到反推上面来,去应证这个公式的正确性。

验证是否是2的n次方,笨的方法就是一直除2,除到最后等于0则就是2的次方,所以公式如上

补位

如果是2个二进制进行运算时,计算机会统一位数,例如

01
011
<------> 
001
011

计算机会将01自动补一位为001去方便运算。

正负

在二进制中,第一位为1的是负数,0是正数。如果没有补零的情况下

10000000000
01111111111

虽然计算后也是0,但它并不是2的n次方,因为第一组二进制是负数。

补充

由上述题补充的另外一道题

给定任意数,计算是2的几次方?
function power($number){
    if ($number < 0) {
        return false;
    }
    
    if (($number & ($number - 1)) == 0) {
        
        // 数学不好的,就看下面的方法
        // $number = decbin($number);
        // return (mb_strlen($number)-1);
        // 数学可以的就看下面的方法
        return floor(log($number,2));
    } else {
        return false;
    }
}
  1. 判断是否是2的n次方
  2. 如果是则将十进制数字转为二进制
  3. 计算总长度-1获取到是2的几次方,按照0的个数来计算

致谢

感谢你看到这里,我也是文中提起的没好好学基础的一名程序员,但当你看到我这篇文章后,希望你也可以提起精神,去重温下基础,对你未来的职业生涯会起作用的。本章内容纯属自己理解,如有出入,请大佬们监督批评,谢谢?

查看原文

赞 44 收藏 30 评论 30

空空 评论了文章 · 2018-07-26

一道看似简单的面试题

clipboard.png

前言

使用PHP,给定一个数,判断这个数是否是二的N次方

这样看似简单的一个面试题, 实际牵出了很多基础知识,本章在为大家补习基础知识的情况下来解答这道题。先亮出答案

function exponentiation($number)
{
    if ($number < 0) {
        return false;
    }
        
    if (($number & ($number - 1)) == 0) {
        return true;
    } else {
        return false;
    }
}
    
exponentiation (2);
exponentiation (8);
exponentiation (16);
exponentiation (32);
exponentiation (128);

这是查阅后的最最标准的答案,其他类似通过循环等等答案就略过把。

运算

实际这道面试题考的并非是算法,而是作为开发人员的你“底子”够不够。
运算符应该是每门语言hello world 后立马要学习的。运算符分很多种,赋值运算符,比较运算符等等。我想大多自学或者培训出道的没有经过系统化学习的童鞋的,对这块的知识应该很薄弱把。当然赋值、比较什么的很熟悉,因业务基本逃不过这些。但对位运算符,你真的吃透了吗?

这道题的考点一是位运算符的使用,上面说过了本题考察的并非算法,而是你对二进制的了解,而在php中能操作二进制的运算符貌似 (我的知识范围内) 只有位运算符

PHP位运算符 (部分)

本道面试题用到了 & 所以不详解其他的运算符,需要更多了解请移步官方
http://php.net/manual/zh/lang...

运算符附加信息
&按位与运算符
^按位异或运算符

按位与

按位与以简单易懂的方式来讲就是二进制位不相同的抵消,相同的保留

clipboard.png

举几个栗子

公式 -> 转为二进制后的公式 -> 二进制结果 -> 十进制结果
2 & 3 -> 0010 & 0011 -> 0010 = 2
10 & 7 -> 1010 & 0111 -> 0010 = 2
32 & 70 -> 0100000 & 1000110 -> 0000000 = 0

按位异或

按位异或以简单易懂的方式来讲就是二进制位相同的抵消,不相同的保留

clipboard.png

举几个栗子

公式 -> 转为二进制后的公式 -> 二进制结果 -> 十进制结果
2 ^ 3 -> 0010 ^ 0011 -> 0001 = 1
10 ^ 7 -> 1010 ^ 0111 -> 1101 = 13
32 ^ 70 -> 0100000 ^ 1000110 -> 1100110 = 102

进制

借用百度百科上的一段话

二进制是计算技术中广泛采用的一种数制。二进制数据是用0和1两个数码来表示的数。它的基数为2,进位规则是“逢二进一”,借位规则是“借一当二”。

二进制本身就是为2这个数字而使用的,所以说这道面试题直指二进制的使用是没错的。2的n次方则就是

n公式结果
122
22x24
32x2x28
42x2x2x216

换算成二进制的表格是

n公式结果
12000010
22x2000100
32x2x2001000
42x2x2x2010000

由此看出2的n次方的二进制最高位是1,其余补0,(n&(n-1))==0 并且 n> 0 的情况下必定是2的n次方,为什么要-1呢?在二进制中每一位必须都不相同&后才会得出0,上述已经讲解了&的运算结果。例如十进制的16

16 & (16 - 1) = 
010000 & (001111) = 0 

不要纠结(n&(n-1))==0 这个公式是怎么来的,作为程序员,我感觉应该把更多时间放到反推上面来,去应证这个公式的正确性。

验证是否是2的n次方,笨的方法就是一直除2,除到最后等于0则就是2的次方,所以公式如上

补位

如果是2个二进制进行运算时,计算机会统一位数,例如

01
011
<------> 
001
011

计算机会将01自动补一位为001去方便运算。

正负

在二进制中,第一位为1的是负数,0是正数。如果没有补零的情况下

10000000000
01111111111

虽然计算后也是0,但它并不是2的n次方,因为第一组二进制是负数。

补充

由上述题补充的另外一道题

给定任意数,计算是2的几次方?
function power($number){
    if ($number < 0) {
        return false;
    }
    
    if (($number & ($number - 1)) == 0) {
        
        // 数学不好的,就看下面的方法
        // $number = decbin($number);
        // return (mb_strlen($number)-1);
        // 数学可以的就看下面的方法
        return floor(log($number,2));
    } else {
        return false;
    }
}
  1. 判断是否是2的n次方
  2. 如果是则将十进制数字转为二进制
  3. 计算总长度-1获取到是2的几次方,按照0的个数来计算

致谢

感谢你看到这里,我也是文中提起的没好好学基础的一名程序员,但当你看到我这篇文章后,希望你也可以提起精神,去重温下基础,对你未来的职业生涯会起作用的。本章内容纯属自己理解,如有出入,请大佬们监督批评,谢谢?

查看原文

认证与成就

  • 获得 3 次点赞
  • 获得 18 枚徽章 获得 0 枚金徽章, 获得 4 枚银徽章, 获得 14 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-01-24
个人主页被 422 人浏览