首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python 学习手册
Python Cookbook
Python 基础教程
Python Sites
PyPI - Python Package Index
http://www.simple-is-better.com/
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
Coding
V2EX  ›  Python

请教一道面试题!

  •  
  •   leisurelylicht · 206 天前 · 3457 次点击
    这是一个创建于 206 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前几天去腾讯面试 Python 的时候遇到一道面试题,如下

    有一个大文件日志,日志内容包含 访问时间 和 访问 IP,问如何统计每分钟访问次数超过 100 次的 IP ?
    

    面试官说手写代码大概是 10 分钟的量。

    我觉得可以用桶排序切割大文件为小文件后再读到内存里统计。但不知道对不对。

    求解是不是有更好的方法?

    第 1 条附言  ·  204 天前

    根据大家提供的思路搞了个demo

    改自@Vegetable的代码

    https://gist.github.com/leisurelicht/[email protected]

    35 回复  |  直到 2019-06-03 18:43:28 +08:00
        1
    oblivious   206 天前
    文件有多大?

    文件能放入内存,的确 10 分钟能写完。文件远大于内存,我这里有一个挺傻的思路:按照每分钟建 24*60 文件,每个文件内再排序。(大文件总不能除以 1440 )还不能丢进内存吧 :)
        2
    leisurelylicht   206 天前
    @oblivious 我觉得他特别说过文件特别大,应该就是指不能一次性读入内存。所以我才往外部排序上考虑的。
        3
    leisurelylicht   206 天前
    @leisurelylicht 我当时就是你这个思路,给他说了,但是面试官没什么反应。我觉得这么做的话涉及到读写文件,10 分钟内也写不完啊。
        4
    di94sh   206 天前 via Android
    我感觉每分钟不是从 1 秒到 60 秒,而是一个窗口,所以需要为这个窗口建缓存,每次读入一秒的数据,然后再过期一秒的数据,之后统计这个窗口内 ip 超过 100 的。
        5
    xinyusir   206 天前
    感觉用 Node 的话 stream 可以完美解决。。。。
        6
    di94sh   206 天前
    @xinyusir #5 文件描述符就是基于流的, 每次读一行就行了.
        7
    binux   206 天前 via iPhone
    日志不都是按照时间排序的吗,一秒一个窗口读一遍就行了啊
        8
    Vegetable   206 天前
    关键词是一个大文件.
    日志默认应该是在时间上连续的,所以无论文件有多大,当前应该只处理 60 秒的数据.不应该有切割文件的操作.
    如果是判断自然分钟,就是从 0 开始读取,读取到下一个分钟结算一下当前分钟的数据,给出达标结果,再继续处理下一个分钟的数据.
    如果是连续 60 秒的话,这个还挺麻烦的,因为 24 小时的不同窗口从 1440 个一下子变成了 86400 个了,会麻烦不少.
        9
    smdbh   206 天前
    我想到的一个: 逐行读取日志文件,对于每个 ip,创建一个文件,写入此行。
    日志文件应该按时间排序的
    所以对于每个 ip 文件,写入的时候就判断与第一行的时间差和之间的行数,比如是否差 100 行且时间差小于 1min
    删掉超过 1 分钟的 log,满足条件,则记录此 ip
        10
    owt5008137   206 天前 via Android
    日志文件。一般时间都是顺序的呀。个人想法:
    可以按 ip,hashmap,如果精度要求是秒级就一个 60 长度的数组统计下次数。如果不是分钟级别精度就可以那直接统计一分钟的次数就好了。
    然后内存够就存内存,不够就落地。
    日志内容顺序扫一遍就完了
        11
    lhx2008   206 天前
    就像 #7 说的,顺序读,然后 Python 这边搞个 dict 做统计应该就可以了,内存只用记录 1 分钟内的所有 IP,过了一分钟就 clear() ,毕竟 10 分钟写不了什么
        12
    oblivious   206 天前
    如果是日志文件按时间排序好了,又不要求连续 60s,数据量不多那就是一个 python dict 就能搞定的事情呀~

    读完这一分钟把 dict 丢掉就好了,hhhh
        13
    qdzzyb   206 天前
    插眼
        14
    g7rhythm   206 天前
    “每分钟访问次数超过 N 次” 与 “单分钟访问次数超过 N 次” 的差别在于一个求均值,一个求峰值。
    既然是求均值,那么最简单的做法就是逐条记录 IP 的总访问次数,然后除以总时间范围 M 分钟,如果文件过大就用流读取。

    然后如果以每一分钟去统计一次,那么就会记录到峰值超过 100 次的 IP,但是总时间内每分钟并没有超过 100 次。
    实际上这可能是一个防止数据采集的办法,防范的重点在于不要被采集过多的数据,而不是特别在意峰值高低。
        15
    lolizeppelin   206 天前
    挺好的题目呀 其实和大访问量的网站怎么过滤高频 IP 一个思路
        16
    lolizeppelin   206 天前
    以 ip 为 key
    pipe 管道去 incr,并计数,超过就是异常 ip

    key 身存时间 1 分钟
        17
    Vegetable   206 天前
    按照分钟统计还是比较简单的,连续 60 秒的比较麻烦,但是逻辑差不多.一般监控也没必要搞那么精确吧

        18
    hhhsuan   206 天前
    思路应该还是比较容易想到的,但我肯定 10 分钟内写不出来,算上 debug 的时间我觉得我要花 1 个小时。
        19
    Pzqqt   206 天前
    Emmm 不知道这样解是否正确

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-

    from collections import defaultdict, Counter

    dic = defaultdict(lambda: set())

    # 假设该日志文件为 log.log
    with open("log.log", "r", encoding="utf-8") as f:
    while True:
    l = f.readline().strip()
    if not l.strip():
    break
    # 假设该日志文件每行的格式为:
    # HH:MM:SS XXX.XXX.XXX.XXX
    try:
    time, ip = l.split(" ")
    except ValueError:
    continue
    dic[time.rsplit(":")[0]].add(ip)

    for time_min, ips in dic:
    for ip, count in Counter(ips):
    if count > 100:
    print(time_min, ip, count)
        20
    binux   206 天前
    哎,真是的,这么简单的问题有什么好讨论的

    def group_by_second(logs):
    start = null
    window = defaultdict(int)
    for time, ip in logs:
    if time - start > 1000:
    yield window
    start = null
    window = {}
    if start is null:
    start = time
    window[ip] += 1

    windows = []
    ip_cnt = defaultdict(int)
    for window in group_by_second(logs):
    windows.append(window)
    if len(windows) > 60:
    for ip, cnt in windows.pop(0):
    ip_cnt[ip] -= cnt
    for ip, cnt in window:
    ip_cnt[ip] += cnt
    if ip_cnt[ip] > 100:
    print ip
        21
    leisurelylicht   206 天前
    @Vegetable
    @binux
    @Pzqqt
    @oblivious
    @lhx2008
    @owt5008137
    按这么考虑还真的是 10 分钟量的代码,是我当时考虑的太复杂了

    @Vegetable 你的代码缩进乱掉了,虽然我还是看懂了
        22
    makdon   206 天前
    问题:有一个大文件日志,日志内容包含 访问时间 和 访问 IP,问如何统计每分钟访问次数超过 100 次的 IP ?
    keyword:日志、大文件、访问频率

    题目并没有说明的内容:
    日志时间跨度:这么大个文件是一天日志还是一分钟日志,还是说其它时间跨度
    分钟切分粒度:假如在 00 分后半分钟访问 50 次,01 分钟前半分钟访问 51 次,算不算“访问次数超过 100 次”

    那就按照最极端的情况来考虑:这个文件是两分钟的日志文件,体积极大,需要任意 60 秒时间段内访问次数不超过 100.

    按照这种极端情况考虑的话,前面的“以分钟切片单位,用字典记录用户访问,每分钟清除一遍字典”之类的方法就不适用了,因为使用的内存量是跟用户量成正比的:假如日志中,有极多用户,每个用户只访问一遍,那字典需要的内存比文件本身一半(假定两分钟的日志)还要大,显然会炸内存。

    我个人认为,在大文件的前提下,只能按照用户进行切片,而不是时间为单位切片,因为每分钟的数据量在这个场景上没有上限,但是用户的数据上限是 100。而且使用用户 IP 作为切片的 Key,可以上 Hadoop、Spark 等分布式计算框架。(时间作为 key 也可以上分布式,不过可能切片太大,计算量集中到单机)。

    当本地操作时,时间 O(n),内存占用 O(1),硬盘占用 O(n);用分布式框架时,时间 O(n),内存 O(n),硬盘 O(1)

    在加个极端条件:这个大文件是 1 个用户在 2 分钟内疯狂访问生成的几十 G 的日志。检查可知,就算是这种情况,按照用户切片,最多只需要记录 100 次访问记录。

    最后,我是云编程玩家,以下是伪代码:


    def map2file(log):
    with open(log['ip]) as file:
    if file.first_line != 'Hit':
    file.append(log)
    check(file)


    def check(file):
    while last_line.time - first_line.time > 60:
    delete(first_line)
    if file.num_of_line >= 100:
    delete_all_lines()
    file.append("Hit")




    if __name__ == '__main__':
    with open('file') as logs:
    for log in logs:
    map2file(log)

    result = []
    for file in working_dir:
    result.append(file.name)




    (其实就是把那个字典记录在文件系统里面
    (是不是觉得我扯了这么多花里胡哨的,10 行代码 9 行 error
    (总感觉我哪里搞错了但是没找出来,有错误请把我往死里锤(反正不可能真的顺着网线砍我(
        23
    makdon   206 天前
    @makdon #22
    果然把我的缩进都删掉了。。。
    主函数写错了,应该是
    if __name__ == '__main__':
    with open('file') as logs:
    for log in logs:
    map2file(log)

    result = new_file
    for file in working_dir:
    if file 点 first_line == 'Hit':
    result 点 append(file 点 name)


    附 gist:
    htt 他 ps:/不 /gist 点 gi 让 thub 点 co 我 m/Mak 插 Don/4aa9 外 138b9f 链 3a5c89c745b6f4b9a5ea82.js
        24
    makdon   206 天前
    没有考虑的极端情况:
    日历不是按时间排序的(不按时间排序就不是日志了吧。。。
    因为服务器时间同步导致的日志时间抖动:例如在 00:00:05 的时候,服务器收到校准时钟信号,把自己时间调到 00:00:00,那出来的日志就是:
    00:00:03:blablabla
    00:00:04:blablabla
    00:00:05:blablabla
    00:00:00:blablabla
    00:00:01:blablabla
        25
    zgl263885   206 天前 via iPhone
    滑动窗口算法,窗口宽度为一分钟,统计窗口内 ip 访问次数超过 100 次的 ip。
        26
    MissThee   206 天前 via iPhone
    虽然不会 puthon,但是感觉用一门语言实现读取文件,计数,1 分钟执行一次的循环任务,应该不会很难吧😅
        27
    Oz2011   206 天前
    每条记录在文件里本身应该是排序的,一条一条读出来,
    ip 为 key, value 是一个时间的有序 list

    同时有另外一个 map, ip 为 key, value 是这个 ip 最早的访问时间 (也可以想办法把两个放到同一个 map 里面)

    每读一个时间,
    if (读出时间 - 60s >= 起始时间),如果 list size > 100 输出,不然这个 ip 就能删掉了。
    else 时间插入对应 ip 的 list
        28
    Oz2011   206 天前
    这个外部排序分割文件没关系啊,顺序处理就行了
        29
    zgq3337   205 天前 via Android
    用 Dict,以 IP 为 key,时间加入列表,每次对应 key 有变化,就对当前索引前 100 次作时间差计算,超过 1 分钟标记
        30
    Pythoner666666   205 天前
    python 不是有迭代器么,每次只读一行到内存中,这样就可以解决了吧
        31
    crazypig14   205 天前
    实际写代码虽然顺序处理为了并发还是得文件切割,切割处小心一点,得有 100 秒的重合
        32
    luckymerlin   205 天前 via Android
    不是 sorted 的先 sort,然后 sliding window + hashmap
        33
    lanshee   191 天前
    with open('filepath') as f:
    for line in f:
    # 假定起始时间为 00:00:00
        34
    lanshee   191 天前
    @lanshee 完犊子.我还没写完.
        35
    lanshee   191 天前
    time_start = None
    with open('filepath') as f:
    for line in f:
    # 1. 找到起始时间格式化字符串通过时间转换转成 int
    # 2. 记录时间差为一分钟的结束时间
    # 3. 判断是否在这一分钟内
    # 4. 如果在,从该行日志中找到 ip
    # 5. 利用 redis 为 ip 记录访问次数
    # 6. 判断该次数是否超过 100,如果超过,就记录

    ps: 求大神指点.
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1539 人在线   最高记录 5043   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.3 · 29ms · UTC 17:06 · PVG 01:06 · LAX 09:06 · JFK 12:06
    ♥ Do have faith in what you're doing.