Skip to content

网络问卷数据清洗怎么做?用IP归属地剔除重复填写和机器人(附Python脚本)

问卷收回来了,五百份,看着挺热闹。结果导出来一排序:同一个 IP 填了 17 份,答题用时最短的 8 秒,还有个 IP 归属地在某地机房,问卷里却自称在杭州读书。这种数据直接进分析,信效度那关基本过不去——导师问一句"样本怎么筛的",当场卡住。

做学术问卷、市场调研的人,八成被这事坑过。这篇讲怎么用 IP 归属地给回收的问卷做清洗:重复填写、机器人作答怎么识别,用一段 Python 脚本批量打标,规则透明可复现,写进论文附录也站得住。

一、问卷数据里常见的"脏"样本

IP 挡不住铁了心刷的人,但能帮你把大部分偷懒的重复填写揪出来。常见几类:

  • 同 IP 重复提交:一个人换微信号、QQ 各填一遍,甚至无痕模式下刷新重填。问卷平台能设"每 IP 限填一次",但 Cookie 限制用无痕模式一绕就过——IP 是绕不掉的。
  • 秒答:几十道量表题,用时不到一分钟,明显没读题。配合同 IP 看,基本坐实。
  • 机房/代理 IP:脚本跑在云服务器上批量提交,IP 归属地是某机房而不是家庭宽带。普通问卷里不多见,一出现就是成片的。
  • 自报城市和 IP 对不上:让你填所在城市,填了上海,IP 却在河南。可能是人真在外地,也可能胡填,得结合其他字段判断,不能一棍子打死。

这些特征里"同 IP"最硬——它是平台记下来的客观数据,不依赖用户自报。

二、方案设计:先统计,再查询,后打标

问卷星这类平台导出 Excel 时自带"IP 地址"列,这是清洗基础。流程三步:

  1. 本地统计:按 IP 分组数出每份出现几次,不调接口,秒出结果。
  2. 查归属地:把去重后的 IP 逐个调 https://ip9.com.cn/get?ip=<ip> 拿省市和运营商。免费版限速 60 次/分钟/IP,脚本里每查一个间隔 1.1 秒,几百个 IP 几分钟跑完。
  3. 打标输出:给每条答卷算标记——同 IP 重复、秒答、归属地、与自报城市是否一致,导出带标记的 CSV 和汇总统计,人工过一遍再决定删谁。

一个容易踩的坑:同 IP ≠ 同一个人。一个宿舍共用一个宽带、一家公司一个出口 IP,几十号人填出来全是同一个 IP。所以脚本只负责"标记",不负责"删除"——删不删,要结合提交时间和自报信息人工判断。这也是学术上说得通的姿势:清洗规则写清楚、可复现,比闷头删数据靠谱。

三、Python 实现

脚本基于 Python 3 + requests:读 CSV → 按 IP 统计 → 查归属地(带缓存,同一 IP 只查一次)→ 逐条打标 → 输出报告。

python
import csv
import re
import time
import requests

# 问卷星导出的列名,用你自己的工具就改成对应列名;没有的列填空
COL_IP = "IP地址"
COL_DUR = "所用时间"        # 问卷星格式如 "3分20秒"
COL_CITY = None             # 问卷里让填了城市的话,填列名,如 "所在城市"
DUR_MAX_SEC = 90            # 低于这个秒数算"秒答",按你的题量调

def parse_dur(text):
    """把 '3分20秒' / '120秒' 解析成秒数,解析不了返回 None"""
    if not text:
        return None
    text = str(text).strip()
    m = re.match(r"(?:(\d+))?(\d+)$", text)
    if m:
        return int(m.group(1) or 0) * 60 + int(m.group(2))
    try:
        return int(text)
    except ValueError:
        return None

def query_geo(ip):
    """查单个 IP 归属地,失败返回 None;免费版限速 60 次/分钟,调用方负责间隔"""
    try:
        r = requests.get("https://ip9.com.cn/get", params={"ip": ip}, timeout=5)
        d = r.json()
        if d.get("ret") == 200:
            data = d["data"]
            return {"prov": data.get("prov", ""), "city": data.get("city", ""),
                    "isp": data.get("isp", "")}
    except Exception:
        pass
    return None

# ---------- 1. 读数据 ----------
rows = []
with open("survey.csv", encoding="utf-8-sig") as f:
    for row in csv.DictReader(f):
        rows.append(row)
print(f"总答卷数: {len(rows)}")

# ---------- 2. 按 IP 统计 ----------
from collections import Counter
ip_counter = Counter(r[COL_IP].strip() for r in rows if r.get(COL_IP))

# ---------- 3. 查询归属地:去重后逐个查,同一 IP 只查一次 ----------
geo_cache = {}
for i, ip in enumerate(ip_counter.keys()):
    geo_cache[ip] = query_geo(ip) or {}
    print(f"查询进度 {i + 1}/{len(ip_counter)}: {ip} -> {geo_cache[ip]}")
    time.sleep(1.1)  # 限速:60 次/分钟,留点余量

# ---------- 4. 逐条打标 ----------
flags = []
for r in rows:
    ip = r.get(COL_IP, "").strip()
    tag = []
    if ip_counter[ip] > 1:
        tag.append(f"同IP共{ip_counter[ip]}份")
    dur = parse_dur(r.get(COL_DUR)) if COL_DUR else None
    if dur is not None and dur < DUR_MAX_SEC:
        tag.append(f"秒答{dur}秒")
    geo = geo_cache.get(ip, {})
    if geo.get("prov"):
        tag.append(f"IP属地:{geo['prov']}{geo['city']} {geo.get('isp', '')}")
    if COL_CITY and geo.get("prov") and r.get(COL_CITY):
        self_city = r[COL_CITY].strip()
        if self_city and self_city not in (geo["city"] + geo["prov"]):
            tag.append(f"自报[{self_city}]与IP地不一致")
    flags.append(";".join(tag) if tag else "正常")

# ---------- 5. 输出带标记的 CSV + 汇总 ----------
with open("survey_flagged.csv", "w", encoding="utf-8-sig", newline="") as f:
    w = csv.writer(f)
    header = list(rows[0].keys()) + ["清洗标记"]
    w.writerow(header)
    for r, flag in zip(rows, flags):
        w.writerow([r.get(h, "") for h in rows[0].keys()] + [flag])

summary = Counter(f.split(";")[0] if ";" in f else ("正常" if f == "正常" else "其他") for f in flags)
print("\n清洗汇总:")
for k, v in summary.most_common():
    print(f"  {k}: {v} 份")

跑完打开 survey_flagged.csv,重点看三拨人:带"同IP"的(数一下是否集中在某个 IP)、"秒答"的、两者叠加的——最后这种基本可以直接剔除。带"自报不一致"的先留着人工核对。

依赖就一个:pip install requests。第 3 步的 time.sleep(1.1) 别删,删了容易触发免费接口的限速,整批查询全废。

四、落地注意事项

  • 回收前拦截比回收后清洗省事:问卷星发布时就能设"每个 IP 限填一次",先开上。它拦明着刷的,绕过 Cookie 的还得靠这套清洗兜底。
  • 阈值跟着题量走:90 秒是针对几十道量表题的。问卷只有 5 道题的话,正常人 30 秒填完,拿 90 秒当"秒答"线就误伤了。
  • 机房 IP 怎么自动识别:免费接口只返回省市和运营商,不返回 IP 类型。要自动识别"机房批量提交",得上返回 ip_type(ISP 家庭/BUS 企业/IDC 机房)的 VIP 版——问卷里清一色 IDC 类型 IP,基本就是脚本刷的。小样本人工看运营商也能看出苗头(比如全是某云厂商的 IP)。
  • 写进论文/报告:清洗规则写清楚——"剔除同一 IP 重复答卷、用时低于 90 秒的答卷共 N 份,有效样本 M 份"。规则透明,审稿人和导师都挑不出毛病。
  • 隐私别忘:IP 属于个人信息,明细别乱传,报告里只留统计结论。

总结

问卷清洗不用搞得多玄乎:IP 去重 + 用时过滤 + 归属地核对,三步就能把大部分"脏"样本标出来。脚本是死的,判断是活的——凡是标记都回原始数据人工过一眼,别让脚本替你"删数据"。

IP 归属地查询用免费接口就够:https://ip9.com.cn/get?ip=<ip> 免注册直接调,返回国家/省市/运营商/经纬度,IPv4、IPv6 都支持,几百份问卷的量级完全在免费额度内,入口见官网 https://www.ip9.com.cn 。要自动识别机房 IP 再考虑 VIP 版。