第11讲 个人信息:爬虫最危险的"地雷区"——50条入罪

0 阅读6分钟

学习目标

跟随架构师律师马原的脚步读完这一讲,你能:

  1. 识别网页中的个人信息并自动脱敏
  2. 理解侵犯公民个人信息罪的入罪标准
  3. 建立一个个人信息风险扫描器

技术原理

"个人信息"的法律定义(《个人信息保护法》第4条):

"以电子或者其他方式记录的与已识别或者可识别的自然人有关的各种信息。"

"各种信息"——范围非常宽。常见的包括:

PERSONAL_INFO_PATTERNS = {
    '手机号': r'1[3-9]\d{9}',
    '身份证号': r'\d{17}[\dXx]|\d{15}',
    '邮箱': r'[\w.-]+@[\w.-]+.\w+',
    '银行卡': r'\d{16,19}',
    '姓名': r'[\u4e00-\u9fff]{2,4}',  # 简化的中文姓名匹配
}

但还有不那么明显的:

  • IP 地址:在某些场景下可识别特定自然人
  • 设备指纹:结合其他信息可识别用户
  • 用户昵称 + 头像 + 地理位置:组合起来可能识别个人
  • 行为数据:购买记录、浏览历史、搜索关键词

去标识化技术

import hashlib

class DataAnonymizer:
    def hash_id(self, raw_id, salt='your_secret_salt'):
        """哈希化标识"""
        return hashlib.sha256(f"{raw_id}{salt}".encode()).hexdigest()[:16]
    
    def mask_phone(self, phone):
        """手机号掩码"""
        if len(phone) == 11:
            return phone[:3] + '****' + phone[7:]
        return phone
    
    def mask_idcard(self, idcard):
        """身份证号掩码"""
        if len(idcard) == 18:
            return idcard[:6] + '********' + idcard[14:]
        return idcard
    
    def k_anonymity_check(self, records, k=5):
        """
        k-匿名检查:确保每个等价类至少有 k 条记录
        这是一个简化的实现,实际应用需要更复杂的算法
        """
        from collections import Counter
        # 按准标识符分组(如:年龄+性别+邮编)
        groups = Counter(tuple(r.get('quasi_id', [])) for r in records)
        violations = {g: c for g, c in groups.items() if c < k}
        return violations

真实判例

上海二中院李某案

李某用爬虫软件从速递公司窃取客户快递信息(丢失件、催收件的问题件信息),然后出售。这些信息包含:寄件人姓名、电话、地址,收件人姓名、电话、地址。

法院认定:构成侵犯公民个人信息罪,情节特别严重,判处有期徒刑五年,并处罚金一百万元

五年实刑。没有缓刑。

另一个极端案例

2023年,某爬虫公司老板接了"企业法人联系方式"的单子。客户说用来做商务拓展。老板写了爬虫,去爬工商系统的全量数据。

结果:七年有期徒刑

工商系统的企业信息里包含了企业法人的姓名、身份证号(或部分身份证号)、联系方式。这些信息属于公民个人信息。全量爬取,数量巨大。七年。

入罪标准

两高司法解释:

信息类型入罪数量
行踪轨迹、通信内容、征信信息、财产信息50条
住宿信息、通信记录、健康生理信息、交易信息500条
其他个人信息5000条

敏感个人信息(身份证号、生物识别、宗教信仰等)标准更低。

爬虫的抓取速度是什么概念?一个设计良好的爬虫,每秒几十个请求。如果每个页面包含一条个人信息,十条就是零点几秒。五十条,一两秒。换句话说,入罪的时间单位不是"天",是"秒"。


法律分析

"可识别性"的判断

不是所有数据都算个人信息。关键是"能不能识别到特定自然人"。

  • 单独一个 IP 地址 → 通常不算(动态 IP 经常变化)
  • IP + 时间戳 + 用户行为 → 可能算
  • 匿名用户昵称 → 通常不算
  • 昵称 + 头像 + 地理位置 + 发帖内容 → 可能算

这个判断没有绝对标准,取决于具体场景和数据组合。

爬虫场景的特殊风险

很多程序员写爬虫的时候,"顺手"抓了个人信息。

比如爬电商商品评价,评价内容里可能有:"快递送到了我公司,某某大厦18层"——这包含了工作地点信息。再比如爬招聘网站,职位信息里没有个人信息,但页面侧边栏可能有发布者的信息。

如果你不做数据清洗,把原始数据直接存进数据库,数据库里可能就躺着大量公民个人信息。检察院做一次数据提取,统计数量,你的量刑档次就定了。


实战输出:个人信息自动识别和脱敏模块

#!/usr/bin/env python3
"""
个人信息风险扫描器
扫描目标内容,识别个人信息并给出风险评级
"""
import re
import json

class PersonalInfoScanner:
    def __init__(self):
        self.patterns = {
            '手机号': {
                'pattern': re.compile(r'(?<![\d])1[3-9]\d{9}(?![\d])'),
                'sensitivity': 'high',
            },
            '身份证号': {
                'pattern': re.compile(r'\b\d{17}[\dXx]|\d{15}\b'),
                'sensitivity': 'high',
            },
            '邮箱': {
                'pattern': re.compile(r'[\w.-]+@[\w.-]+.\w+'),
                'sensitivity': 'medium',
            },
            '银行卡': {
                'pattern': re.compile(r'\b\d{16,19}\b'),
                'sensitivity': 'high',
            },
            '车牌号': {
                'pattern': re.compile(r'[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼][A-Z][A-Z0-9]{4,5}[A-Z0-9挂学警港澳]'),
                'sensitivity': 'medium',
            },
        }
    
    def scan(self, content):
        """扫描内容,返回发现的个人信息"""
        findings = {}
        for name, config in self.patterns.items():
            matches = config['pattern'].findall(content)
            if matches:
                findings[name] = {
                    'count': len(matches),
                    'sensitivity': config['sensitivity'],
                    'examples': matches[:3],  # 只保留前3个示例
                }
        return findings
    
    def assess_risk(self, findings):
        """评估风险等级"""
        high_count = sum(
            f['count'] for f in findings.values() 
            if f['sensitivity'] == 'high'
        )
        total_count = sum(f['count'] for f in findings.values())
        
        if high_count >= 10 or total_count >= 50:
            return 'critical', '🔴 极高风险:可能已触犯侵犯公民个人信息罪'
        elif high_count >= 5 or total_count >= 20:
            return 'high', '🟠 高风险:建议立即停止并清理数据'
        elif total_count > 0:
            return 'medium', '🟡 中风险:需要数据脱敏处理'
        else:
            return 'low', '🟢 未发现明显的个人信息'
    
    def desensitize(self, content):
        """对内容进行脱敏处理"""
        result = content
        # 手机号脱敏
        result = re.sub(
            r'(?<![\d])1[3-9]\d{9}(?![\d])',
            lambda m: m.group()[:3] + '****' + m.group()[7:],
            result
        )
        # 身份证号脱敏
        result = re.sub(
            r'\b(\d{6})\d{8}(\d{4})\b',
            r'\1********\2',
            result
        )
        # 邮箱脱敏
        result = re.sub(
            r'([\w.-])[\w.-]*@([\w.-]+.\w+)',
            r'\1***@\2',
            result
        )
        return result

def main():
    # 示例:扫描一个网页内容
    sample_content = """
    联系人:张三,手机号:13800138000,身份证号:110101199001011234
    邮箱:zhangsan@example.com,车牌:京A12345
    备用联系人:李四,手机号:13900139000
    """
    
    scanner = PersonalInfoScanner()
    findings = scanner.scan(sample_content)
    risk_level, risk_msg = scanner.assess_risk(findings)
    
    print("=== 个人信息扫描结果 ===")
    print(f"\n风险等级: {risk_msg}")
    print(f"\n发现项目:")
    for name, info in findings.items():
        print(f"  {name}: {info['count']}条 (敏感度: {info['sensitivity']})")
    
    print(f"\n脱敏后内容:")
    print(scanner.desensitize(sample_content))

if __name__ == '__main__':
    main()

架构师律师建议在爬虫系统中集成这个扫描器:抓取内容后先扫描,发现个人信息立即告警并触发脱敏流程。