面试官常问的软件测试Python问题

本文转载自霍格沃兹测试学院优秀学员ling_tianxia的学习笔记,原文链接:
http://qrcode.testing-studio.com/f?from=csdn2&url=https://ceshiren.com/tag/%E7%B2%BE%E5%8D%8E%E5%B8%96 转载请注明出处

66.了解数据库的三范式么?
答: 经过研究和对使用中问题的总结,对于设计数据库提出了一些规范,这些规范被称为范式一般需要遵守下面3范式即可: \
    - 第一范式(1NF):强调的是列的原子性,即列不能够再分成其他几列。
    - 第二范式(2NF):首先是1NF,另外包含两部分内容,一是表必须有一个主键;二是没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。
    - 第三范式(3NF):首先是2NF,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列A依赖于非主键列B,非主键列B依赖于主键的情况。

67.了解分布式锁么
答: 分布式锁是控制分布式系统之间的同步访问共享资源的一种方式。 对于分布式锁的目标,我们必须首先明确三点:
    - 任何一个时间点必须只能够有一个客户端拥有锁。
    - 不能够有死锁,也就是最终客户端都能够获得锁,尽管可能会经历失败。
    - 错误容忍性要好,只要有大部分的Redis实例存活,客户端就应该能够获得锁。
    分布式锁的条件
        互斥性:分布式锁需要保证在不同节点的不同线程的互斥
        可重入性:同一个节点上的同一个线程如果获取了锁之后,能够再次获取这个锁。
        锁超时:支持超时释放锁,防止死锁
        高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
        支持阻塞和非阻塞:可以实现超时获取失败,tryLock(longtimeOut) 支持公平锁和非公平锁
    分布式锁的实现方案
        1、数据库实现(乐观锁)
        2、基于zookeeper的实现
        3、基于Redis的实现(推荐)

68.用Python实现一个Reids的分布式锁的功能
答: REDIS分布式锁实现的方式:SETNX + GETSET, NX是NoteXists的缩写,如SETNX命令就应该理解为:SET if NoteXists。 多个进程执行以下Redis命令:
    SETNX
    lock.foo < current
    Unix
    time + lock
    timeout + 1 >
    如果SETNX返回1,说明该进程获得锁,SETNX将键lock.foo的值设置为锁的超时时间(当前时间 + 锁的有效时间)。
    如果SETNX返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试SETNX操作,以获得锁。

    import time
    import redis
    from conf.config import REDIS_HOST, REDIS_PORT, REDIS_PASSWORD

    class RedisLock:
        def __init__(self):
            self.conn = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, db=1)
            self._lock = 0
            self.lock_key = ""

        @staticmethod
        def my_float(timestamp):
        """
        Args:
            timestamp:
        Returns:
            float或者0
            如果取出的是None,说明原本锁并没人用,getset已经写入,返回0,可以继续操作。
        """
            if timestamp:
                return float(timestamp)
            else:
                # 防止取出的值为None,转换float报错
                return 0

        @staticmethod
        def get_lock(cls, key, timeout=10):
            cls.lock_key = f"{key}_dynamic_lock"
            while cls._lock != 1:
                timestamp = time.time() + timeout + 1
                cls._lock = cls.conn.setnx(cls.lock_key, timestamp)
                # if 条件中,可能在运行到or之后被释放,也可能在and之后被释放
                # 将导致 get到一个None,float失败。
                if cls._lock == 1 or (
                        time.time() > cls.my_float(cls.conn.get(cls.lock_key)) and
                        time.time() > cls.my_float(cls.conn.getset(cls.lock_key, timestamp))):
                    break
                else:
                    time.sleep(0.3)

        @staticmethod
        def release(cls):
            if cls.conn.get(cls.lock_key) and time.time() < cls.conn.get(cls.lock_key):
                cls.conn.delete(cls.lock_key)

    def redis_lock_deco(cls):
        def _deco(func):
            def __deco(*args, **kwargs):
                cls.get_lock(cls, args[1])
                try:
                    return func(*args, **kwargs)
                finally:
                    cls.release(cls)

            return __deco

        return _deco

    @redis_lock_deco(RedisLock())
    def my_func():
        print("myfunc() called.")
        time.sleep(20)

    if __name__ == "__main__":
        my_func()

69.写一段Python使用mongo数据库创建索引的代码:
答:
    # -*- coding: utf-8 -*-
    # @Time : 2018/12/28 10:01 AM
    import pymongo
    db_configs = {
        'type': 'mongo',
        'host': '地址',
        'port': '端口',
        'user': 'spider_data',
        'passwd': '密码',
        'db_name': 'spider_data'
    }

    class Mongo():
        def __init__(self, db=db_configs["db_name"], username=db_configs["user"],
                     password=db_configs["passwd"]):
            self.client = pymongo.MongoClient(f'mongodb://{db_configs["host"]}:{db_configs["port"]}')
            self.username = username
            self.password = password
            if self.username and self.password:
                self.db1 = self.client[db].authenticate(self.username, self.password)
            self.db1 = self.client[db]

        def add_index(self):
        # 通过create_index添加索引
            self.db1.test.create_index([('name', pymongo.ASCENDING)], unique=True)

        def get_index(self, ):
        # 查看索引列表
            indexlist = self.db1.test.list_indexes()
            for index in indexlist:
                print(index)

    if __name__ == '__main__':
        m = Mongo()
        m.add_index()
        print(m.get_index())

- 高级特性

70.函数装饰器有什么作用?请列举说明?
答: 装饰器就是一个函数,它可以在不需要做任何代码变动的前提下给一个函数增加额外功能,启动装饰的效果。 它经常用于有切面需求的场景,
    比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。 下面是一个日志功能的装饰器
        from functools import wraps
        def log(label):
            def decorate(func):
                @wraps(func)
                def _wrap(*args, **kwargs):
                    try:
                        func(*args, **kwargs)
                        print("name", func.__name__)
                    except Exception as e:
                        print(e.args)
                return _wrap
            return decorate

        @log("info")
        def foo(a, b, c):
            print(a + b + c)
            print("in foo")
        decorate=decorate(foo)

        if __name__ == '__main__':
            foo(1, 2, 3)
            decorate()

71.Python垃圾回收机制?
答:Python不像C++,Java等语言一样,他们可以不用事先声明变量类型而直接对变量进行赋值。对Python语言来讲,对象的类型和内存都是在运行时确定的。
这也是为什么我们称Python语言为动态类型的原因。主要体现在下面三个方法:
    1.引用计数机制
    2.标记 - 清除
    3.分代回收

72.魔法函数_call_怎么使用?
答: _call_可以把类实例当做函数调用。 使用示例如下
    class Bar:
        def __call__(self, *args, **kwargs):
            print('in call')

    if __name__ == '__main__':
        b = Bar()
        b()

73.如何判断一个对象是函数还是方法?
答:看代码已经结果就懂了
    from types import MethodType, FunctionType
    class Bar:
        def foo(self):
            pass
    def foo2():
        pass
    def run():
        print("foo 是函数", isinstance(Bar().foo, FunctionType))
        print("foo 是方法", isinstance(Bar().foo, MethodType))
        print("foo2 是函数", isinstance(foo2, FunctionType))
        print("foo2 是方法", isinstance(foo2, MethodType))

    if __name__ == '__main__':
        run()
    '''
    foo是函数,False
    foo是方法,True
    foo2是函数,True
    foo2是方法,False
    '''

74. @ classmethod和 @ staticmethod用法和区别
答:
    相同之处:@staticmethod和 @classmethod都可以直接类名.方法名()来调用,不用在示例化一个类。
    @classmethod:我们要写一个只在类中运行而不在实例中运行的方法。如果我们想让方法不在实例中运行,可以这么做:
        def iget_no_of_instance(ins_obj):
            return ins_obj.__class__.no_inst
        class Kls(object):
            no_inst = 0
            def __init__(self):
                Kls.no_inst = Kls.no_inst + 1
        ik1 = Kls()
        ik2 = Kls()
        print(iget_no_of_instance(ik1))

    @staticmethod:经常有一些跟类有关系的功能但在运行时又不需要实例和类参与的情况下需要用到静态方法IND = 'ON'
        class Kls(object):
            def __init__(self, data):
                self.data = data
            @staticmethod
            def check_ind():
                return (IND == 'ON')
            def do_reset(self):
                if self.check_ind():
                    print('Reset done for:', self.data)
            def set_db(self):
                if self.check_ind():
                    self.db = 'New db connection'
                    print('DB connection made for: ', self.data)

        ik1 = Kls(12)
        ik1.do_reset()
        ik1.set_db()

75.Python中的接口如何实现?
答: 接口提取了一群类共同的函数,可以把接口当做一个函数的集合,然后让子类去实现接口中的函数。但
是在Python中根本就没有一个叫做interface的关键字,如果非要去模仿接口的概念,可以使用抽象类来实现。抽象类是一个特殊的类,它的特殊之处在于只能被继承,不能被实例化。
使用abc模块来实现抽象类。

76.Python中的反射了解么?
答:Python的反射机制设定较为简单,一共有四个关键函数分别是getattr、hasattr、setattr、delattr。

77.metaclass作用?以及应用场景?
答: metaclass即元类,metaclass是类似创建类的模板,所有的类都是通过他来create的(调用new),这使得你可以自由的控制创建类的那个过程,实现你所需要的功能。
我们可以使用元类创建单例模式和实现ORM模式。

78.hasattr()、getattr()、setattr()的用法
答:这三个方法属于Python的反射机制里面的,
    hasattr可以判断一个对象是否含有某个属性,
    getattr可以充当get获取对象属性的作用,
    而setattr可以充当person.name = "liming"的赋值操作。代码示例如下:
        class Person():
            def __init__(self):
                self.name = "liming"
                self.age = 12
            def show(self):
                print(self.name)
                print(self.age)
            def set_name(self):
                setattr(Person, "sex", "男")
            def get_name(self):
                print(getattr(self, "name"))
                print(getattr(self, "age"))
                print(getattr(self, "sex"))

        def run():
            if hasattr(Person, "show"):
                print("判断 Person 类是否含有 show 方法")
                Person().set_name()
                Person().get_name()

        if __name__ == '__main__':
            run()

79.请列举你知道的Python的魔法方法及用途。
答:
- 1.__init__:类的初始化方法。它获取任何传给构造器的参数(比如我们调用x = SomeClass(10, ‘foo’) , __init__就会接到参数10和 ‘foo’ 。
    __init__在Python的类定义中用的最多。
- 2.__new__:__new__是对象实例化时第一个调用的方法,它只取下cls参数,并把其他参数传给__init__ 。
    __new__很少使用,但是也有它适合的场景,尤其是当类继承自一个像元组或者字符串这样不经常改变的类型的时候.
- 3.__del__:__new__和__init__是对象的构造器,__del__是对象的销毁器。它并非实现了语句del x(因此该语句不等同于x.__del__())。
    而是定义了当对象被垃圾回收时的行为。当对象需要在销毁时做一些处理的时候这个方法很有用,比如socket对象、文件对象。
    但是需要注意的是,当Python解释器退出但对象仍然存活的时候,__del__并不会执行。 所以养成一个手工清理的好习惯是很重要的,比如及时关闭连接。

80.如何知道一个Python对象的类型?
答:可以通过type方法

81.Python的传参是传值还是传址?
答:Python中的传参即不是传值也不是传地址,传的是对象的引用。

82.Python中的元类(metaclass)使用举例
答:可以使用元类实现一个单例模式,代码如下:
    class Singleton(type):
        def __init__(self, *args, **kwargs):
            print("in __init__")
            self.__instance = None
            super(Singleton, self).__init__(*args, **kwargs)

        def __call__(self, *args, **kwargs):
            print("in __call__")
            if self.__instance is None:
                self.__instance = super(Singleton, self).__call__(*args, **kwargs)
            return self.__instance

    class Foo(metaclass=Singleton):
        pass  # 在代码执行到这里的时候,元类中的__new__方法和__init__方法其实已经被执行了,而不是在 Foo 实例化的时候执行。且仅会执行一次。

    foo1 = Foo()
    foo2 = Foo()
    print(foo1 is foo2)

83.简述any()和all()方法
答:
    any(x):判断x对象是否为空对象,如果都为空、0、false,则返回false,如果不都为空、0、false,则返回true。
    all(x):如果all(x)参数x对象的所有元素不为0、''、False或者x为空对象,则返回True,否则返回False。

84.filter方法求出列表所有奇数并构造新列表,a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
答:
    a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    print(list(filter(lambda x: x % 2 == 1, a)))
    其实现在不推荐使用filter, map等方法了,一般列表生成式就可以搞定了。

85.什么是猴子补丁?
答: 猴子补丁(monkeypatching):在运行时动态修改模块、类或函数,通常是添加功能或修正缺陷。猴子补丁在代码运行时内存中)发挥作用,不会修改源码,因此只对当前运行的程序实例有效。
因为猴子补丁破坏了封装,而且容易导致程序与补丁代码的实现细节紧密耦合,所以被视为临时的变通方案,不是集成代码的推荐方式。大概是下面这样的一个效果
    def post():
        print("this is post")
        print("想不到吧")
    class Http():
        @classmethod
        def get(self):
            print("this is get")
    def main():
        Http.get = post  # 动态的修改了 get 原因的功能,
    if __name__ == '__main__':
        main()
        Http.get()

86.在Python中是如何管理内存的?
答:
    垃圾回收:Python不像C + +,Java等语言一样,他们可以不用事先声明变量类型而直接对变量进行赋值。对Python语言来讲,对象的类型和内存都是在运行时确定的。
这也是为什么我们称Python语言为动态类型的原因(这里我们把动态类型可以简单的归结为对变量内存地址的分配是在运行时自动判断变量类型并对变量进行赋值。
    引用计数:Python采用了类似Windows内核对象一样的方式来对内存进行管理。每一个对象,都维护这一个对指向该对对象的引用的计数。
当变量被绑定在一个对象上的时候,该变量的引用计数就是1,(还有另外一些情况也会导致变量引用计数的增加),系统会自动维护这些标签,并定时扫描,当某标签的引用计数变为0的时候,该对就会被回收。
    内存池机制:Python的内存机制以金字塔行,1、2层主要有操作系统进行操作;第0层是C中的malloc,free等内存分配和释放函数进行操作;
第1层和第2层是内存池,有Python的接口函数PyMem_Malloc函数实现,当对象小于256K时有该层直接分配内存;第3层是最上层,也就是我们对Python对象的直接操作在C中如果频繁的调用malloc与free时,
是会产生性能问题的.再加上频繁的分配与释放小块的内存会产生内存碎片。Python在这里主要干的工作有:如果请求分配的内存在1~256字节之间就使用自己的内存管理系统, 否则直接使用malloc。
这里还是会调用malloc分配内存,但每次会分配一块大小为256k的大块内存。经由内存池登记的内存到最后还是会回收到内存池,并不会调用C的free释放掉以便下次使用。
对于简单的Python对象,例如数值、字符串,元组(tuple不允许被更改)采用的是复制的方式(深拷贝?),也就是说当将另一个变量B赋值给变量A时,虽然A和B的内存空间仍然相同,但当A的值发生变化时,会
重新给A分配空间,A和B的地址变得不再相同。

87.当退出Python时是否释放所有内存分配?
答:不是的,循环引用其他对象或引用自全局命名空间的对象的模块,在Python退出时并非完全释放。另外,也不会释放c库保留的内存部分

- 正则表达式

88. (1)使用正则表达式匹配出 < html > < h1\ > www.baidu.com < / h1 > < / html > 中的地址,(2)a = "张明 98 分",用re.sub,将98替换为100
答:
第一问答案
    import re

    source = "

www.baidu.com

" pat = re.compile("

(.*?)

") print(pat.findall(source)[0]) 第二问答案 import re s = "张明 98 分" print(re.sub(r"\d+", "100", s)) 89.正则表达式匹配中(. *)和(. *?)匹配区别? 答:(.*)为贪婪模式极可能多的匹配内容, (.* ?)为非贪婪模式又叫懒惰模式,一般匹配到结果就好,匹配字符的少为主,示例代码如下 import re s = "
文本 1
文本 2
" pat1 = re.compile(r"\
(.*?)\
") print(pat1.findall(s)) pat2 = re.compile(r"\
(.*)\
") print(pat2.findall(s)) # 输出 # ['文本 1', '文本 2'] # ['文本 1
文本 2'] 90.写一段匹配邮箱的正则表达式 答:关于邮箱的匹配这个还真的是一个永恒的话题。 电子邮件地址有统一的标准格式:用户名 @ 服务器域名。用户名表示邮件信箱、注册名或信件接收者的用户标识,@符号后是你使用的邮件服务器的域名。 @可以读成“at”,也就是“在”的意思。整个电子邮件地址可理解为网络中某台服务器上的某个用户的地址。用户名,可以自己选择。 由字母a~z(不区分大小写)、数字0~9、点、减号或下划线组成;只能以数字或字母开头和结尾。与你使用的网站有关,代表邮箱服务商。例如网易的有@163.com;新浪有@vip.sina.com等。 网上看到了各种各样的版本,都不确定用哪个,于是自己简单的总结了一个。大家有更好的欢迎留言。 r"^[a-zA-Z0-9]+[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$" 下面解释上面的表达式,首先强调一点关于\w的含义,\w匹配英文字母和俄语字母或数字或下划线或汉字。 注意 ^ []和[ ^]的区别,[]表示字符集合,^ []表示以[]内的任意字符集开始,[ ^]表示。^ [a - zA - Z0 - 9] +:这里注意 ^ []和[ ^]的, 第一个 ^ 表示已什么开头,第二个[] 的 ^ 表示不等于[]内。所以这段表示以英文字母和数字开头,后面紧跟的 +,限定其个数 >= 1个。 [a - zA - Z0 - 9. + -] +:表示匹配英文字母和数字开头以及. + -, 的任意一个字符,并限定其个数 >= 1个。为了考虑 @ 前面可能出现. + -(但是不在开头出现)。 @就是邮箱必备符号了@[a - zA - Z0 - 9 -] +\.:前面的不用说了,后面的.表示.转义了, 也是必备符号。 [a - zA - Z0 - 9 -.]+:$符表示以什么结束, 这里表示以英文字和数字或 - .1个或多个结尾。 来个例子验证一波: import re plt = re.compile(r"^[a-zA-Z0-9]+[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") b = plt.findall('[email protected]') print(b) 网上找了个验证邮件地址的通用正则表达式(符合RFC5322标准) (?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\]) - 其他内容 91.解释一下Python中pass语句的作用? 答:pass实际上就是一个占位符,在写一个函数但是不确定里面写啥的时候,这个时候可以使用pass。示例如下 def foo(): pass 92.简述你对input()函数的理解 答:在Python3中input函数可以接收用户输入的字符串。然后根据程序的需要转换成所需格式即可。 93.Python中的 is 和 == 答:先说 == 它的作用是判断两个对象的值是否相同,然后说 is。 is 表示的谁是谁,这也就意味着对象完全相等。我们知道一个对象有各自的内存地址和对应的值,当内存地址和值都相同的时候使用 is 可以得到结果 True。另外需要注意的下面两点特殊的情况。这些变量很可能在许多程序中使用。 通过池化这些对象,Python可以防止对一致使用的对象进行内存分配调用。 1.介于数字 - 5和256之间的整数 2.字符串仅包含字母、数字或下划线 94.Python中的作用域 答:Python中,一个变量的作用域总是由在代码中被赋值的地方所决定当Python遇到一个变量的话它会按照这的顺序进行搜索本地作用域(Local) - -->当前作用域被嵌入的本地作用域(Enclosinglocals) --->全局 / 模块作用域(Global) - -->内置作用域(Built - in) 95.三元运算写法和应用场景? 答:Python中的三元运算又称三目运算,是对简单的条件语句的简写。 是一种比较Pythonic的学法,形式为:val = 1 if 条件成立 else 2 代码示例如下: a = 2 b = 5 # 普通写法 if a > b: val = True else : val = False # 改为三元运算符后 val = a if a > b else b print(val) # 5 96.了解enumerate么? 答:enumerate 可以在迭代一个对象的时候,同时获取当前对象的索引和值。 代码示例如下 from string import ascii_lowercase s = ascii_lowercase for index, value in enumerate(s): print(index, value) 97.列举5个Python中的标准模块 答: pathlib:路径操作模块,比os模块拼接方便。 urllib:网络请求模块,包括对url的结构解析。 asyncio: Python的异步库,基于事件循环的协程模块。 re:正则表达式模块。 itertools:提供了操作生成器的一些模块。 98.如何在函数中设置一个全局变量 答:# 通过使用 global 对全局变量进行修改。 n = 0 def foo(): global n n = 100 foo() print(n) x = 0 之前我在视频教程中对这块做了个讲解,具体点击下方链接 https: // www.bilibili.com / video / av50865713 99.pathlib的用法举例 答:pathlib是面向对象的文件系统路径,可以对文件以及文件的其他属性进行操作。比较喜欢的一点是路径拼接符"/"的使用。 from pathlib import Path, PurePath p = Path('.') # 列出子目录 for x in p.iterdir(): print(f"path:{x},is_dir:{x.is_dir()}") # 路径拼接 >>> p = PurePath('/etc') >>> p / 'init.d' / 'apache2' PurePosixPath('/etc/init.d/apache2') >>> q = PurePath('bin') >>> '/usr' / q PurePosixPath('/usr/bin') 100.Python中的异常处理,写一个简单的应用场景 答: 比如在计算除法中出现为0的情况出现异常 try: 1 / 0 except ZeroDivisionError as e: print(e.args) 101.Python中递归的最大次数,那如何突破呢? 答:Python有递归次数限制,默认最大次数为1000。通过下面的代码可以突破这个限制 import sys sys.setrecursionlimit(1500) # set the maximum depth as 1500 另外需要注意的是 sys.setrecursionlimit() 只是修改解释器在解释时允许的最大递归次数,此外,限制最大递归次数的还和操作系统有关。 102.什么是面向对象的mro 答:Python是支持面向对象编程的,同时也是支持多重继承的。一般我们通过调用类对象的mro()方法获取其继承关系。 103.isinstance作用以及应用场景? 答:isinstance是判断一个对象是否为另一个对象的子类的,例如我们知道在Python3中bool类型其实是int的子类,所以我们可以对其检测。 print(isinstance(True, int)) 104.什么是断言?应用场景? 答:在Python中是断言语句assert 实现此功能,一般在表达式为True的情况下,程序才能通过。 # assert()方法,断言成功,则程序继续执行,断言失败,则程序报错 # 断言能够帮助别人或未来的你理解代码, # 找出程序中逻辑不对的地方。一方面, # 断言会提醒你某个对象应该处于何种状态, # 另一方面,如果某个时候断言为假, # 会抛出 AssertionError 异常,很有可能终止程序。 def foo(a): assert a == 2, Exception("不等于 2") print("ok", a) if __name__ == '__main__': foo(1) 105.lambda 表达式格式以及应用场景? 答:lambda 表达式其实就是一个匿名函数, 在函数编程中经常作为参数使用。 例子如下 a = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] a_1 = list(map(lambda x:x[0], a)) 106.新式类和旧式类的区别 答:Python2.x中默认都是经典类,只有显式继承了object才是新式类,Python3.x中默认都是新式类,经典类被移除,不必显式的继承object。 新式类都从object继承,经典类不需要。 新式类的MRO(methodresolutionorder基类搜索顺序)算法采用C3算法广度优先搜索,而旧式类的MRO算法是采用深度优先搜索。新式类相同父类只执行一次构造函数,经典类重复执行多次。 107.dir()是干什么用的? 答:当在使用某一个对象不知道有哪些属性或者方法可以使用时,此时可以通过dir()方法进行查看。 108.一个包里有三个模块,demo1.py、demo2.py、demo3.py,但使用from tools import *导入模块时,如何保证只有demo1、demo3被导入了。 答: 增加_init_.py文件,并在文件中增加:__all__ = ['demo1', 'demo3'] 109.列举5个Python中的异常类型以及其含义 答:AttributeError对象没有这个属性 NotImplementedError尚未实现的方法 StopIteration迭代器没有更多的值 TypeError对类型无效的操作 IndentationError缩进错误 110.copy和deepcopy的区别是什么? 答: copy.copy()浅拷贝,只拷贝父对象,不会拷贝对象的内部的子对象。 copy.deepcopy()深拷贝,拷贝对象及其子对象。 111.代码中经常遇到的 * args, ** kwargs含义及用法。 答: 在函数定义中使用 * args和 ** kwargs传递可变长参数。 *args用来将参数打包成tuple给函数体调用。 ** kwargs打包关键字参数成dict给函数体调用。 112.Python中会有函数或成员变量包含单下划线前缀和结尾,和双下划线前缀结尾,区别是什么? 答: "单下划线"开始的成员变量叫做保护变量,意思是只有类对象和子类对象自己能访问到这些变量; "双下划线"开始的是私有成员,意思是只有类对象自己能访问,连子类对象也不能访问到这个数据。 以单下划线开头(_foo)的代表不能直接访问的类属性,需通过类提供的接口进行访问,不能用“from xxx import *”而导入;以双下划线开头的(__foo)代表类的私有成员; 以双下划线开头和结尾的(_foo)代表Python里特殊方法专用的标识,如__init__()代表类的构造函数。 113.w、a +、wb文件写入模式的区别 答: w表示写模式支持写入字符串,如果文件存在则覆盖。 a + 和w的功能类型不过如果文件存在的话内容不会覆盖而是追加。 wb是写入二进制字节类型的数据。 114.举例sort和sorted的区别 答: 相同之处sort和sorted都可以对列表元素排序, sort()与sorted()的不同在于,sort是在原位重新排列列表,而sorted()是产生一个新的列表。 sort是应用在list上的方法,sorted可以对所有可迭代的对象进行排序操作。 list的sort方法返回的是对已经存在的列表进行操作,而内建函数sorted方法返回的是一个新的list,而不是在原来的基础上进行的操作。 115.什么是负索引? 答:负索引一般表示的是从后面取元素。 116.pprint模块是干什么的? 答:pprint是print函数的美化版,可以通过import pprint导入。示例如下 import pprint pprint.pprint("this is pprint") 117.解释一下Python中的赋值运算符 答:通过下面的代码列举出所有的赋值运算符 a = 7 a += 1 print(a) a -= 1 print(a) a *= 2 print(a) a /= 2 print(a) a **= 2 print(a) a //= 3 print(a) a %= 4 print(a) 118.解释一下Python中的逻辑运算符 答:Python中有三个逻辑运算符: and 、 or 、not print(False and True) # False print(7 < 7 or True) # True print(not 2 == 2) # False 119.讲讲Python中的位运算符 答:按位运算符是把数字看作二进制来进行计算的。Python中的按位运算法则如下: 下表中变量a为60,b为13,二进制格式如下: a = 0011 1100 b = 0000 1101 ----------------- a & b = 0000 1100 a | b = 0011 1101 a ^ b = 0011 0001 ~a = 1100 0011 enter image description here 120.在Python中如何使用多进制数字? 答: 我们在Python中,除十进制外还可以使用二进制、八进制和十六进制,二进制数字由0和1组成,我们使用0b或0B前缀表示二进制数print(int(0b1010)) # 10 使用bin()函数将一个数字转换为它的二进制形式print(bin(0xf)) # 0b1111 八进制数由数字0 - 7组成,用前缀0o或0O表示8进制数print(oct(8)) # 0o10 十六进数由数字0 - 15组成,用前缀0x或者0X表示16进制数print(hex(16)) # 0x10,print(hex(15)) # 0xf 121.怎样声明多个变量并赋值? 答:Python是支持多个变量赋值的,代码示例如下 # 对变量 a,b,c 声明并赋值a, b, c = 1, 2, 3 算法和数据结构 122. 已知:AList = [1, 2, 3] BSet = {1, 2, 3} (1)从AList和BSet中查找4,最坏时间复杂度哪个大? (2)从AList和BSet中插入4,最坏时间复杂度哪个大? 答: (1) 对于查找,列表和集合的最坏时间复杂度都是O(n),所以一样的。 (2)列表操作插入的最坏时间复杂度为o(n), 集合为o(1),所以Alist大。 set是哈希表所以操作的复杂度基本上都是o(1)。 123.用Python实现一个二分查找的函数 答: def binary_search(arr, target): n = len(arr) left = 0 right = n - 1 while left <= right : mid = (left + right) // 2 if arr[mid] < target: left = mid + 1 elif arr[mid] > target: right = mid - 1 else : print(f"index:{mid},value:{arr[mid]}") return True return False if __name__ == '__main__': l = [1, 3, 4, 5, 6, 7, 8] binary_search(l, 8) 124.Python单例模式的实现方法 答:实现单例模式的方法有多种,之前再说元类的时候用call方法实现了一个单例模式,另外Python的模块就是一个天然的单例模式,这里我们使用new关键字来实现一个单例模式。 """ 通过 new 函数实现简单的单例模式。 """ class Book: def __new__(cls, title): if not hasattr(cls, "_ins"): cls._ins = super().__new__(cls) print('in __new__') return cls._ins def __init__(self, title): print('in __init__') super().__init__() self.title = title if __name__ == '__main__': b = Book('The Spider Book') b2 = Book('The Flask Book') print(id(b)) print(id(b2)) print(b.title) print(b2.title) 125.使用Python实现一个斐波那契数列 答: 斐波那契数列:数列从第3项开始,每一项都等于前两项之和。 def fibonacci(num): a, b = 0, 1 l = [a, b] for i in range(num): a, b = b, a + b l.append(b) return l if __name__ == '__main__': print(fibonacci(10)) 126.找出列表中的重复数字 答: """ 从头扫到尾,只要当前元素值与下标不同,就做一次判断,numbers[i]与 numbers[numbers[i]], 相等就认为找到了重复元素,返回 true,否则就交换两者,继续循环。直到最后还没找到认为没找到重复元素。 """ # -*- coding:utf-8 -*- class Solution: def duplicate(self, numbers): if numbers is None or len(numbers) <= 1: return False use_set = set() duplication = {} for index, value in enumerate(numbers): if value not in use_set: use_set.add(value) else: duplication[index] = value return duplication if __name__ == '__main__': s = Solution() d = s.duplicate([1, 2, -3, 4, 4, 95, 95, 5, 2, 2, -3, 7, 7, 5]) print(d) 127.找出列表中的单个数字 答: def find_single(l :list): result = 0 for v in l: result ^= v if result == 0: print("没有落单元素") else : print("落单元素", result) if __name__ == '__main__': l = [1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6] find_single(l) 128.写一个冒泡排序 答: def bubble_sort(arr): n = len(arr) for i in range(n - 1): for j in range(n - i - 1):. if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] if __name__ == '__main__': l = [1, 2, 3, 4, 5, 55, 6, 3, 4, 5, 6] bubble_sort(l) print(l) 129.写一个快速排序 答: def quick_sort(arr, first, last): if first >= last: return mid_value = arr[first] low = first high = last while low < high: while low < high and arr[high] >= mid_value: high -= 1 # 游标左移 arr[low] = arr[high] while low < high and arr[low] < mid_value: low += 1 arr[high] = arr[low] arr[low] = mid_value quick_sort(arr, first, low - 1) quick_sort(arr, low + 1, last) if __name__ == '__main__': l = [1, 2, 3, 4, 5, 55, 6, 3, 4, 5, 6] quick_sort(l, 0, len(l) - 1) print(l) 130.写一个拓扑排序 答: """ 对应于该图的拓扑排序。每一个有向无环图都至少存在一种拓扑排序。 """ import pysnooper from typing import Mapping @pysnooper.snoop() def topological_sort(graph:Mapping): # in_degrees = {'a': 0, 'b': 0, 'c': 0, 'd': 0, 'e': 0, 'f': 0} in_degrees = dict((u, 0) for u in graph) for u in graph: for v in graph[u]: # 根据键找出值也就是下级节点 in_degrees[v] += 1 # 对获取到的下级节点的入度加 1 # 循环结束之后的结果: {'a': 0, 'b': 1, 'c': 1, 'd': 2, 'e': 1, 'f': 4} Q = [u for u in graph if in_degrees[u] == 0] # 入度为 0 的节点 in_degrees_zero = [] while Q: u = Q.pop() # 默认从最后一个移除 in_degrees_zero.append(u) # 存储入度为 0 的节点 for v in graph[u]: in_degrees[v] -= 1 # 删除入度为 0 的节点,以及移除其指向 if in_degrees[v] == 0: Q.append(v) return in_degrees_zero if __name__ == '__main__': # 用字典的键值表示图的节点之间的关系,键当前节点。值是后续节点。 graph_dict = { 'a': 'bf', # 表示 a 指向 b 和 f 'b': 'cdf', 'c': 'd', 'd': 'ef', 'e': 'f', 'f': ''} t = topological_sort(graph_dict) print(t) 131.Python实现一个二进制计算 答: """ 二进制加法 """ def binary_add(a:str, b: str): return bin(int(a, 2) + int(b, 2))[2:] if __name__ == '__main__': num1 = input("输入第一个数,二进制格式:\n") num2 = input("输入第二个数,二进制格式:\n") print(binary_add(num1, num2)) 132.有一组“+”和“-”符号,要求将“+”排到左边,“-”排到右边,写出具体的实现方法。 答: """ 有一组“+”和“-”符号,要求将“+”排到左边,“-”排到右边,写出具体的实现方法。 如果让+等于 0,-等于 1 不就是排序了么。 """ from collections import deque from timeit import Timer s = "++++++----+++----" # 方法一 def func1(): new_s = s.replace("+", "0").replace("-", "1") result = "".join(sorted(new_s)).replace("0", "+").replace("1", "-") return result # 方法二 def func2(): q = deque() left = q.appendleft right = q.append for i in s: if i == "+": left("+") elif i == "-": right("-") # 方法三 def func3(): data = list(s) start_index = 0 end_index = 0 count = len(s) while start_index + end_index < count: if data[start_index] == '-': data[start_index], data[count - end_index - 1] = data[count - end_index - 1], data[start_index] end_index += 1 else : start_index += 1 return "".join(data) if __name__ == '__main__': timer1 = Timer("func1()", "from __main__ import func1") print("func1", timer1.timeit(1000000)) timer2 = Timer("func2()", "from __main__ import func2") print("func2", timer2.timeit(1000000)) timer3 = Timer("func3()", "from __main__ import func3") print("func3", timer3.timeit(1000000)) # 1000000 测试结果 # func1 1.39003764 # func2 1.593012875 # func3 3.3487415590000005 # func1 的方式最优,其次是 func2 133.单链表反转 答: """ 单链表反转 """ class Node: def __init__(self, val=None): self.val = val self.next = None class SingleLinkList: def __init__(self, head=None): """链表的头部""" self._head = head def add(self, val:int): """ 给链表添加元素 :param val: 传过来的数字 :return: """ # 创建一个节点 node = Node(val) if self._head is None: self._head = node else : cur = self._head while cur.next is not None: cur = cur.next # 移动游标 cur.next = node # 如果 next 后面没了证明以及到最后一个节点了 def traversal(self): if self._head is None: return else : cur = self._head while cur is not None: print(cur.val) cur = cur.next def size(self): """ 获取链表的大小 :return: """ count = 0 if self._head is None: return count else : cur = self._head while cur is not None: count += 1 cur = cur.next return count def reverse(self): """ 单链表反转 思路: 让 cur.next 先断开即指向 none,指向设定 pre 游标指向断开的元素,然后 cur.next 指向断开的元素,再把开始 self._head 再最后一个元素的时候. :return: """ if self._head is None or self.size() == 1: return else : pre = None cur = self._head while cur is not None: post = cur.next cur.next = pre pre = cur cur = post self._head = pre # 逆向后的头节点 if __name__ == '__main__': single_link = SingleLinkList() single_link.add(3) single_link.add(5) single_link.add(6) single_link.add(7) single_link.add(8) print("对链表进行遍历") single_link.traversal() print(f"size:{single_link.size()}") print("对链表进行逆向操作之后") single_link.reverse() single_link.traversal() 134.交叉链表求交点 答: # Definition for singly-linked list. class ListNode: def __init__(self, x): self.val = x self.next = None class Solution: def getIntersectionNode(self, headA, headB): """ :tye head1, head1: ListNode :rtye: ListNode """ if headA is not None and headB is not None: cur1, cur2 = headA, headB while cur1 != cur2: cur1 = cur1.next if cur1 is not None else headA cur2 = cur2.next if cur2 is not None else headB return cur1 cur1、cur2,2个指针的初始位置是链表headA、headB头结点,cur1、cur2两个指针一直往后遍历。 直到cur1指针走到链表的末尾,然后cur1指向headB; 直到cur2指针走到链表的末尾,然后cur2指向headA; 然后再继续遍历; 每次cur1、cur2指向None,则将cur1、cur2分别指向headB、headA。 循环的次数越多,cur1、cur2的距离越接近,直到cur1等于cur2。则是两个链表的相交点。 135.用队列实现栈ww 答: 下面代码分别使用1个队列和2个队列实现了栈。 from queue import Queue # 使用 2 个队列实现 class MyStack: def __init__(self): """ Initialize your data structure here. """ # q1 作为进栈出栈,q2 作为中转站 self.q1 = Queue() self.q2 = Queue() def push(self, x): """ Push element x onto stack. :type x: int :rtype: void """ self.q1.put(x) def pop(self): """ Removes the element on top of the stack and returns that element. :rtype: int """ while self.q1.qsize() > 1: self.q2.put(self.q1.get()) # 将 q1 中除尾元素外的所有元素转到 q2 中 if self.q1.qsize() == 1: res = self.q1.get() # 弹出 q1 的最后一个元素 self.q1, self.q2 = self.q2, self.q1 # 交换 q1,q2 return res def top(self): """ Get the top element. :rtype: int """ while self.q1.qsize() > 1: self.q2.put(self.q1.get()) # 将 q1 中除尾元素外的所有元素转到 q2 中 if self.q1.qsize() == 1: res = self.q1.get() # 弹出 q1 的最后一个元素 self.q2.put(res) # 与 pop 唯一不同的是需要将 q1 最后一个元素保存到 q2 中 self.q1, self.q2 = self.q2, self.q1 # 交换 q1,q2 return res def empty(self): """ Returns whether the stack is empty. :rtype: bool """ return not bool(self.q1.qsize() + self.q2.qsize()) # 为空返回 True,不为空返回 False # 使用 1 个队列实现 class MyStack2(object): def __init__(self): """ Initialize your data structure here. """ self.sq1 = Queue() def push(self, x): """ Push element x onto stack. :type x: int :rtype: void """ self.sq1.put(x) def pop(self): """ Removes the element on top of the stack and returns that element. :rtype: int """ count = self.sq1.qsize() if count == 0: return False while count > 1: x = self.sq1.get() self.sq1.put(x) count -= 1 return self.sq1.get() def top(self): """ Get the top element. :rtype: int """ count = self.sq1.qsize() if count == 0: return False while count: x = self.sq1.get() self.sq1.put(x) count -= 1 return x def empty(self): """ Returns whether the stack is empty. :rtype: bool """ return self.sq1.empty() if __name__ == '__main__': obj = MyStack2() obj.push(1) obj.push(3) obj.push(4) print(obj.pop()) print(obj.pop()) print(obj.pop()) print(obj.empty()) 136.找出数据流的中位数 答:对于一个升序排序的数组,中位数为左半部分的最大值,右半部分的最小值,而左右两部分可以是无需的,只要保证左半部分的数均小于右半部分即可。因此,左右两半部分分别可用最大堆、最小堆实现。 如果有奇数个数,则中位数放在左半部分;如果有偶数个数,则取左半部分的最大值、右边部分的最小值之平均值。 分两种情况讨论: 当目前有偶数个数字时,数字先插入最小堆,然后选择最小堆的最小值插入最大堆(第一个数字插入左半部分的最小堆)。 当目前有奇数个数字时,数字先插入最大堆,然后选择最大堆的最大值插入最小堆。 最大堆:根结点的键值是所有堆结点键值中最大者,且每个结点的值都比其孩子的值大。 最小堆:根结点的键值是所有堆结点键值中最小者,且每个结点的值都比其孩子的值小。 # -*- coding:utf-8 -*- from heapq import * class Solution: def __init__(self): self.maxheap = [] self.minheap = [] def Insert(self, num): if (len(self.maxheap) + len(self.minheap)) & 0x1: # 总数为奇数插入最大堆 if len(self.minheap) > 0: if num > self.minheap[0]: # 大于最小堆里的元素 heappush(self.minheap, num) # 新数据插入最小堆 heappush(self.maxheap, -self.minheap[0]) # 最小堆中的最小插入最大堆 heappop(self.minheap) else : heappush(self.maxheap, -num) else : heappush(self.maxheap, -num) else : # 总数为偶数 插入最小堆 if len(self.maxheap) > 0: # 小于最大堆里的元素 if num < -self.maxheap[0]: heappush(self.maxheap, -num) # 新数据插入最大堆 heappush(self.minheap, -self.maxheap[0]) # 最大堆中的最大元素插入最小堆 heappop(self.maxheap) else : heappush(self.minheap, num) else : heappush(self.minheap, num) def GetMedian(self, n=None): if (len(self.maxheap) + len(self.minheap)) & 0x1: mid = self.minheap[0] else : mid = (self.minheap[0] - self.maxheap[0]) / 2.0 return mid if __name__ == '__main__': s = Solution() s.Insert(1) s.Insert(2) s.Insert(3) s.Insert(4) print(s.GetMedian()) 137.二叉搜索树中第K小的元素 答: ??二叉搜索树(BinarySearchTree),又名二叉排序树(BinarySortTree)。 ? 二叉搜索树是具有有以下性质的二叉树:? 若左子树不为空,则左子树上所有节点的值均小于或等于它的根节点的值。 若右子树不为空,则右子树上所有节点的值均大于或等于它的根节点的值。 左、右子树也分别为二叉搜索树。二叉搜索树按照中序遍历的顺序打印出来正好就是排序好的顺序。所以对其遍历一个节点就进行计数,计数达到k的时候就结束。 class TreeNode: def __init__(self, x): self.val = x self.left = None self.right = None class Solution: count = 0 nodeVal = 0 def kthSmallest(self, root, k): """ :type root: TreeNode :type k: int :rtype: int """ self.dfs(root, k) return self.nodeVal def dfs(self, node, k): if node != None: self.dfs(node.left, k) self.count = self.count + 1 if self.count == k: self.nodeVal = node.val # 将该节点的左右子树置为 None,来结束递归,减少时间复杂度 node.left = None node.right = None self.dfs(node.right, k) 爬虫相关 138.在requests模块中,requests.content和requests.text什么区别 答: requests.content获取的是字节,requests.text获取的是文本内容。 139.简要写一下lxml模块的使用方法框架 答: from lxml import html source = ''' < div class ="nam" > < span > 中国 < / span > < / div > ''' root = html.fromstring(source) _content = root.xpath("string(//div[@class='nam'])") if _content and isinstance(_content, list): content = _content[0] elif isinstance(_content, str): content = _content print(content) 140.说一说scrapy的工作流程 答:首先还是先看张图enter image description here 以www.baidu.com为例: 首先需要知道的事各个模块之间调用都是通过引擎进行的。 spider把百度需要下载的第一个url:www.baidu.com交给引擎。引擎把url交给调度器排序入队处理。调度器把处理好的request返回给引擎。 通过引擎调动下载器,按照下载中间件的设置下载这个request。下载器下载完毕结果返回给引擎(如果失败:不好意思,这个request下载失败,然后引擎告诉调度器,这个 request下载失败了,你记录一下,我们待会儿再下载。)引擎调度spider,把按照Spider中间件处理过了的请求,交给spider处理。spider把处理好的url和item传给引擎。 引擎根据不同的类型调度不同的模块,调度ItemPipeline处理item。把url交给调度器。 然后从第4步开始循环,直到获取到你需要的信息,注意!只有当调度器中不存在任何 request了,整个程序才会停止。 141.scrapy的去重原理 答:scrapy本身自带一个去重中间件,scrapy源码中可以找到一个dupefilters.py去重器。里面有个方法叫做request_seen,它在scheduler(发起请求的第一时间) 的时候被调用。它代码里面调用了request_fingerprint方法(就是给request生成一个指纹)。就是给每一个传递过来的url生成一个固定长度的唯一的哈希值。但是这种量级千万到亿的级别内存是可以应付的。 142.scrapy中间件有几种类,你用过哪些中间件 答: scrapy的中间件理论上有三种(SchdulerMiddleware, SpiderMiddleware, DownloaderMiddleware)。 在应用上一般有以下两种爬虫中间件SpiderMiddleware:主要功能是在爬虫运行过程中进行一些处理。 下载器中间件DownloaderMiddleware:这个中间件可以实现修改User - Agent等headers信息,处理重定向,设置代理,失败重试,设置cookies等功能。 143. 你写爬虫的时候都遇到过什么?反爬虫措施,你是怎么解决的? 答:Headers: 从用户的headers进行反爬是最常见的反爬虫策略。Headers是一种区分浏览器行为和机器行为中最简单的方法,还有一些网站会对Referer (上级链接) 进行检测(机器行为不太可能通过链接跳转实现)从而实现爬虫。 相应的解决措施:通过审查元素或者开发者工具获取相应的headers然后把相应的headers传输给Python的requests, 这样就能很好地绕过。 IP限制一些网站会根据你的IP地址访问的频率,次数进行反爬。也就是说如果你用单一的IP地址访问频率过高,那么服务器会在短时间内禁止这个IP访问。 解决措施:构造自己的IP代理池,然后每次访问时随机选择代理(但一些IP地址不是非常稳定,需要经常检查更新)。 UA限制UA是用户访问网站时候的浏览器标识,其反爬机制与ip限制类似。 解决措施:使用随机UA验证码反爬虫或者模拟登陆 验证码:这个办法也是相当古老并且相当的有效果,如果一个爬虫要解释一个验证码中的内容,这在以前通过简单的图像识别是可以完成的,但是就现在来讲,验证码的干扰线, 噪点都很多,甚至还出现了人类都难以认识的验证码。 相应的解决措施:验证码识别的基本方法:截图,二值化、中值滤波去噪、分割、紧缩重排(让高矮统一)、字库特征匹配识别。(Python的PIL库或者其他),复杂的情况需求接入打码平台。 Ajax动态加载网页:不希望被爬虫拿到的数据使用Ajax动态加载,这样就为爬虫造成了绝大的麻烦,如果一个爬虫不具备js引擎,或者具备js引擎,但是没有处理js返回的方案, 或者是具备了js引擎,但是没办法让站点显示启用脚本设置。基于这些情况,ajax动态加载反制爬虫还是相当有效的。Ajax动态加载的工作原理是:从网页的url加载网页的源代码之后, 会在浏览器里执行JavaScript程序。这些程序会加载出更多的内容,并把这些内容传输到网页中。这就是为什么有些网页直接爬它的URL时却没有数据的原因。 处理方法:找对应的ajax接口,一般数据返回类型为json。 cookie限制:一次打开网页会生成一个随机cookie,如果再次打开网页这个cookie不存在,那么再次设置,第三次打开仍然不存在,这就非常有可能是爬虫在工作了。 解决措施:在headers挂上相应的cookie或者根据其方法进行构造(例如从中选取几个字母进行构造)。如果过于复杂,可以考虑使用selenium模块(可以完全模拟浏览器行为)。 144.为什么会用到代理? 答:如果使用同一个ip去不断的访问的网站的话, 会很容易被封ip,严重的永久封禁,导致当前的访问不了该网站。不只是通过程序,通过浏览器也无法访问。 145.代理失效了怎么处理? 答:一般通过大家代理池来实现代理切换等操作,来实现时时使用新的代理ip,来避免代理失效的问题。 146.列出你知道header的内容以及信息 答: User - Agent:User - Agent的内容包含发出请求的用户信息。 Accept:指定客户端能够接收的内容类型。 Accept - Encoding:指定浏览器可以支持的web服务器返回内容压缩编码类型。 Accept - Language:浏览器可接受的语言。 Connection:表示是否需要持久连接。(HTTP1.1默认进行持久连接)。 Content - Length:请求的内容长度。 If - Modified - Since:如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304代码。 Referer:先前网页的地址,当前请求网页紧随其后,即来路。 147.说一说打开浏览器访问www.baidu.com获取到结果,整个流程。 答: 浏览器向DNS服务器发送baidu.com域名解析请求。 DNS服务器返回解析后的ip给客户端浏览器,浏览器向该ip发送页面请求。 DNS服务器接收到请求后,查询该页面, 并将页面发送给客户端浏览器。 客户端浏览器接收到页面后,解析页面中的引用,并再次向服务器发送引用资源请求。 服务器接收到资源请求后,查找并返回资源给客户端。 客户端浏览器接收到资源后,渲染,输出页面展现给用户。 148.爬取速度过快出现了验证码怎么处理 答:一般在爬取过程中出现了验证码根据不同的情况,处理不一样。 如果在一开始访问就有验证码, 那么就想办法绕开验证码, 比如通过wap端或者app去发现其他接口等, 如果不行就得破解验证码了,复杂验证码就需要接入第三方打码平台了。 如果开始的时候没有验证码,爬了一段时间才出现验证码,这个情况就要考虑更换代理ip了。 可能因为同一个访问频率高导致的。 149.scrapy和scrapy - redis有什么区别?为什么选择redis数据库? 答: scrapy是一个Python爬虫框架,爬取效率极高,具有高度定制性,但是不支持分布式。而scrapy - redis一套基于redis数据库、运行在scrapy框架之上的组件,可以让 scrapy支持分布式策略,Slaver端共享Master端redis数据库里的item队列、请求队列和请求指纹集合。 为什么选择redis数据库,因为redis支持主从同步,而且数据都是缓存在内存中的,所以基于redis的分布式爬虫,对请求和数据的高频读取效率非常高。 150.分布式爬虫主要解决什么问题 答:使用分布式主要目的就是为了给爬虫加速。解决了单个ip的限制,宽带的影响,以及CPU的使用情况和io等一系列操作 151.写爬虫是用多进程好?还是多线程好? 为什么? 答: 多线程,因为爬虫是对网络操作属于io密集型操作适合使用多线程或者协程。 152.解析网页的解析器使用最多的是哪几个 答:lxml,pyquery,bs4等 153.需要登录的网页,如何解决同时限制ip,cookie, session(其中有一些是动态生成的)在不使用动态爬取的情况下? 答: 解决限制IP可以搭建代理IP地址池、adsl拨号使用等。不适用动态爬取的情况下可以使用反编译JS文件获取相应的文件,或者换用其他平台(比如手机端)看看是否可以获取相应的 json文件,一般要学会习惯性的先找需要爬取网站的h5端页面,看看有没有提供接口,进而简化操作。 154.验证码的解决? 答: 图形验证码:干扰、杂色不是特别多的图片可以使用开源库Tesseract进行识别,太过复杂的需要借助第三方打码平台。 点击和拖动滑块验证码可以借助selenium、无图形界面浏览器(chromedirver或者phantomjs)和pillow包来模拟人的点击和滑动操作,pillow可以根据色差识别需要滑动的位置。 155.使用最多的数据库(mysql,mongodb,redis等),对他的理解? 答: MySQL数据库:开源免费的关系型数据库,需要实现创建数据库、数据表和表的字段,表与表之间可以进行关联(一对多、多对多),是持久化存储。 mongodb数据库:是非关系型数据库,数据库的三元素是,数据库、集合、文档,可以进行持久化存储,也可作为内存数据库,存储数据不需要事先设定格式,数据以键值对的形式存储。 redis数据库:非关系型数据库,使用前可以不用设置格式,以键值对的方式保存,文件格式相对自由,主要用与缓存数据库,也可以进行持久化存储。 网络编程 156.TCP和UDP的区别? 答: UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息。 优点:UDP速度快、操作简单、要求系统资源较少,由于通讯不需要连接,可以实现广播发送。 缺点:UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,也不重复发送,不可靠。 TCP是面向连接的通讯协议,通过三次握手建立连接,通讯完成时四次挥手。 优点:TCP在数据传递时,有确认、窗口、重传、阻塞等控制机制,能保证数据正确性,较为可靠。 缺点:TCP相对于UDP速度慢一点,要求系统资源较多。 157.简要介绍三次握手和四次挥手 答: 三次握手:第一次握手:主机A发送同步报文段(SYN)请求建立连接。 第二次握手:主机B听到连接请求,就将该连接放入内核等待队列当中,并向主机A发送针对SYN的确认ACK, 同时主机B也发送自己的请求建立连接(SYN)。 第三次握手:主机A针对主机B的SYN的确认应答ACK。 四次挥手:第一次挥手:当主机A发送数据完毕后,发送FIN结束报文段。 第二次挥手:主机B收到FIN报文段后,向主机A发送一个确认序号ACK(为了防止在这段时间内,对方重传FIN报文段)。 第三次挥手:主机B准备关闭连接,向主机A发送一个FIN结束报文段。 第四次挥手:主机A收到FIN结束报文段后,进入TIME_WAIT状态。并向主机B发送一个ACK表示连接彻底释放。 除此之外经常看的问题还有,为什么2、3次挥手不能合在一次挥手中? 那是因为此时A虽然不再发送数据了,但是还可以接收数据,B可能还有数据要发送给A,所以两次挥手不能合并为一次。 158.什么是粘包? socket中造成粘包的原因是什么? 哪些情况会发生粘包现象? 答:TCP是流式协议,只有字节流,流是没有边界的,根部就不存在粘包一说,一般粘包都是业务上没处理好造成的。但是在描述这个现象的时候,可能还得说粘包。 TCP粘包通俗来讲,就是发送方发送的多个数据包,到接收方后粘连在一起,导致数据包不能完整的体现发送的数据。导致TCP粘包的原因,可能是发送方的原因,也有可能是接受方的原因。 发送方由于TCP需要尽可能高效和可靠,所以TCP协议默认采用Nagle算法,以合并相连的小数据包,再一次性发送,以达到提升网络传输效率的目的。 但是接收方并不知晓发送方合并数据包,而且数据包的合并在TCP协议中是没有分界线的,所以这就会导致接收方不能还原其本来的数据包。 接收方TCP是基于“流”的。网络传输数据的速度可能会快过接收方处理数据的速度,这时候就会导致,接收方在读取缓冲区时,缓冲区存在多个数据包。 在TCP协议中接收方是一次读取缓冲区中的所有内容,所以不能反映原本的数据信息。 一般的解决方案大概下面几种: 发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。 包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。包头加上包体长度。包头是定长的4个字节,说明了包体的长度。 接收对等方先接收包体长度,依据包体长度来接收包体。 并发 159.举例说明concurrent.future的中线程池的用法 答: from concurrent.futures import ThreadPoolExecutor import requests URLS = ['http://www.163.com', 'https://www.baidu.com/', 'https://github.com/'] def load_url(url): req = requests.get(url, timeout=60) print(f'{url} page is {len(req.content))} bytes') with ThreadPoolExecutor(max_workers=3) as pool: pool.map(load_url, URLS) print('主线程结束') 160. 说一说多线程,多进程和协程的区别。 答: 概念: 进程:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间, 不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。 线程:线程是进程的一个实体, 是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源, 只拥有一点在运行中必不可少的资源(如程序计数器, 一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。 线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。 协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方, 在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。 区别: 进程与线程比较: 线程是指进程内的一个执行单元, 也是进程内的可调度实体。 线程与进程的区别: 1) 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间, 而进程有自己独立的地址空间 2) 资源拥有:进程是资源分配和拥有的单位, 同一个进程内的线程共享进程的资源 3) 线程是处理器调度的基本单位, 但进程不是 4) 二者均可并发执行 5) 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制 协程与线程进行比较: 1) 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样 Python 中则能使用多核 CPU。 2) 线程进程都是同步机制,而协程则是异步 3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态 161. 简述 GIL 答: GIL:全局解释器锁。每个线程在执行的过程都需要先获取 GIL,保证同一时刻只有一个线程可以执行代码。 线程释放 GIL 锁的情况:在 IO 操作等可能会引起阻塞的 systemcall 之前, 可以暂时释放 GIL,但在执行完毕后, 必须重新获取 GIL, Python3.x 使用计时器(执行时间达到阈值后,当前线程释放 GIL)或 Python2.x,tickets 计数达到 100 。 Python 使用多进程是可以利用多核的 CPU 资源的。多线程爬取比单线程性能有提升,因为遇到 IO 阻塞会自动释放 GIL 锁。 162. 进程之间如何通信 答: 可以通过队列的形式,示例如下 from multiprocessing import Queue, Process import time, random # 要写入的数据 list1 =["java", "Python", "JavaScript"] def write(queue): """ 向队列中添加数据 :param queue: :return: """ for value in list1: print(f"正在向队列中添加数据-->{value}") # put_nowait 不会等待队列有空闲位置再放入数据,如果数据放入不成功就直接崩溃,比如数据满了。put 的话就会一直等待 queue.put_nowait(value) time.sleep(random.random()) def read(queue): while True: # 判断队列是否为空 if not queue.empty(): # get_nowait 队列为空,取值的时候不等待,但是取不到值那么直接崩溃了 value = queue.get_nowait() print(f'从队列中取到的数据为-->{value}') time.sleep(random.random()) else : break if __name__ == '__main__': # 父进程创建出队列,通过参数的形式传递给子进程 # queue = Queue(2) queue = Queue() # 创建两个进程 一个写数据 一个读数据 write_data = Process(target=write, args=(queue,)) read_data = Process(target=read, args=(queue,)) # 启动进程 写入数据 write_data.start() # 使用 join 等待写数据结束 write_data.join() # 启动进程 读取数据 print('*' * 20) read_data.start() # 使用 join 等待读数据结束 read_data.join() print('所有的数据都写入并读取完成。。。') 163.IO多路复用的作用? 答: 阻塞I / O只能阻塞一个I / O操作,而I / O复用模型能够阻塞多个I / O操作,所以才叫做多路复用。 I / O多路复用是用于提升效率,单个进程可以同时监听多个网络连接IO。 在IO密集型的系统中, 相对于线程切换的开销问题,IO多路复用可以极大的提升系统效率。 164.select、poll、epoll模型的区别? 答: select,poll,epoll都是IO多路复用的机制。I / O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪), 能够通知程序进行相应的读写操作。 select模型: select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在 Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。 poll模型: poll和select的实现非常类似,本质上的区别就是存放fd集合的数据结构不一样。select在一个进程内可以维持最多1024个连接,poll在此基础上做了加强, 可以维持任意数量的连接。但select和poll方式有一个很大的问题就是,我们不难看出来select是通过轮询的方式来查找是否可读或者可写,打个比方,如果同时有 100万个连接都没有断开,而只有一个客户端发送了数据,所以这里它还是需要循环这么多次,造成资源浪费。所以后来出现了epoll系统调用。 epoll模型: epoll是select和poll的增强版,epoll同poll一样,文件描述符数量无限制。但是也并不是所有情况下epoll都比select / poll 好,比如在如下场景:在大多数客户端都很活跃的情况下,系统会把所有的回调函数都唤醒,所以会导致负载较高。既然要处理这么多的连接,那倒不如select遍历简单有效。 165.什么是并发和并行? 答:“并行是指同一时刻同时做多件事情,而并发是指同一时间间隔内做多件事情”。 并发与并行是两个既相似而又不相同的概念:并发性,又称共行性,是指能处理多个同时性活动的能力;并行是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行, 也亦是说并发事件之间不一定要同一时刻发生。并发的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。 并行性指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。并行,是每个CPU运行一个程序。 166.一个线程1让线程2去调用一个函数怎么实现 答: import threading def func1(t2): print('正在执行函数func1') t2.start() def func2(): print('正在执行函数func2') if __name__ == '__main__': t2 = threading.Thread(target=func2) t1 = threading.Thread(target=func1, args=(t2,)) t1.start() 167.解释什么是异步非阻塞? 答: 异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。 对于异步调用,调用的返回并不受调用者控制。 非阻塞是这样定义的,当线程遇到I / O操作时,不会以阻塞的方式等待I / O操作的完成或数据的返回,而只是将I / O请求发送给操作系统,继续执行下一条语句。当操作系统完成 I / O操作时,以事件的形式通知执行I / O操作的线程,线程会在特定时候处理这个事件。简答理解就是如果程序不会卡住,可以继续执行,就是说非阻塞的。 168.threading.local的作用? 答: threading.local()这个方法是用来保存一个全局变量,但是这个全局变量只有在当前线程才能访问,如果你在开发多线程应用的时候, 需要每个线程保存一个单独的数据供当前线程操作,可以考虑使用这个方法,简单有效。代码示例 import threading import time a = threading.local() # 全局对象 def worker(): a.x = 0 for i in range(200): time.sleep(0.01) a.x += 1 print(threading.current_thread(), a.x) for i in range(20): threading.Thread(target=worker).start() Git面试题 169.说说你知道的git命令 答: git init:该命令将创建一个名为.git的子目录, 这个子目录含有你初始化的Git仓库中所有的必须文件, 这些文件是Git仓库的骨干 git clone url:将服务器代码下载到本地 git pull:将服务器的代码拉到本地进行同步,如果本地有修改会产生冲突。 git push:提交本地修改的代码到服务器 git checkout - b branch:创建并切换分支 git status:查看修改状态 git add 文件名:提交到暂存区 git commit - m "提交内容":输入提交的注释内容 git log:查看提交的日志情况 170.git如何查看某次提交修改的内容 答:我们首先可以git log显示历史的提交列表之后我们用git show便可以显示某次提交的修改内容同样git show filename可以显示某次提交的某个内容的修改信息。

本文转载自霍格沃兹测试学院优秀学员ling_tianxia的学习笔记,原文链接:
http://qrcode.testing-studio.com/f?from=csdn2&url=https://ceshiren.com/tag/%E7%B2%BE%E5%8D%8E%E5%B8%96 转载请注明出处

你可能感兴趣的:(测试工程师,软件测试,黑盒测试,渗透测试,python)