[项目记录]积分大模型00-爬虫获取数据🦎

0. 项目背景

目前乡村的治理广泛采用一种叫“积分制”的工具,简单来说就是每个村民干了什么可以加分、做错什么就要减分,一定时间后统计积分数据,村民根据积分不同可以得到一些奖励(生活用品等奖品,有些与分红挂钩)。

但因为具体的积分制度是由基层工作人员制定的,这些积分制规则可能会存在不公平、无效的问题,即“公平性”和“有效性”两方面的问题。所以希望通过大模型对这些积分规则进行“公平性”和“有效性”两方面的评价,识别出积分规则的问题,帮助基层工作人员制定更加好的规则。

本篇主要是记录一下我从小程序爬虫获取数据的过程。


1. 获取接口

这部份是师姐做的,师姐提供了可以跑的代码(包括接口和cookie),十分感谢师姐提供源码。

我也尝试自己使用 charles 抓了一下,可以找到这个接口,但是直接访问会提示 {"code":400,"requestId":"","message":"参数错误"},应该是请求头的验证有问题,我太菜了没能研究出来怎么获取 cookie 的,等后面研究。

接口:https://xxx.xxx.xxx.xxx/score/score-detail/home-page?villageId={}&revisionId=%20HTTP/1.1


2. 代理池

在爬虫中一个 IP 频繁访问很容易被 ban,我又希望可以做一个多线程并发的爬虫程序,在网上查询到可以利用 http/https 代理的方式避免本地 IP 频繁访问被 ban。

在 GitHub 一顿寻找后发现了一个代理池项目 proxy_pool ,该项目提供一个代理池框架,并且会自动爬虫获取免费的代理。

但遗憾的是原作者已经不维护获取免费代理了,获取免费代理功能基本失效。好消息是原项目中的 Pull requestsIssues 中有很多人提供了新的免费代理代码,我 fork 了原项目并且合并了原项目中的部份 Pull requests ,在此感谢 @Jerry12228@wencan,新的项目在此

然后我就得到了:

api method Description params
/ GET api介绍 None
/get GET 随机获取一个代理 可选参数: ?type=https 过滤支持https的代理
/pop GET 获取并删除一个代理 可选参数: ?type=https 过滤支持https的代理
/all GET 获取所有代理 可选参数: ?type=https 过滤支持https的代理
/count GET 查看代理数量 None
/delete GET 删除代理 ?proxy=host:ip

3. 断点续爬 & 多线程

3.1 断点续爬

折腾好代理池后,按以往的经验,绝对会出现一个爬虫失败整个程序奔溃的情况,而且有爬虫失败的漏网之鱼很难处理。

所以我决定使用一个 json 文件来记录进度,格式如下,主键为villageId,如46,然后记录爬虫结果信息status(状态码200为done、其他为none、异常为fail)、 爬虫时间time、 爬虫时使用代理proxy

如果出现异常(比如代理连接失败、访问超时等)每个 villageId 将尝试 8 次,且在最后一次不使用任何代理,使用本地 IP 进行爬虫(这个是因为代理池里全是网络上爬取的免费代理 ,质量实在有点次,为了避免因为代理问题造成失败设置)。

启动爬虫程序时,读取 1_WeChatSpider/spider/logs/progress.json 文件,获取已经爬好的 villageIdprogress,每个id爬取时检查是否已在progress.json中且statusdonenone,是则跳过。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"46": {
"status": "done",
"time": "2025-02-07 22:39:19",
"proxy": "116.172.66.186:12701"
},
"1": {
"status": "none",
"time": "2025-02-07 23:14:32",
"proxy": "196.192.76.185:3128"
},
...
}

3.2 多线程

据说接口中的 villageId 是一个很大范围的数值,如果让很多个线程按顺序竞争爬取的话可能会经常会被锁卡住。

我想起以前用过一个多线程下载器IDM,它是将文件切成线程数量个小块,然后每个小块内部就是一个线程在下载,就不会出现竞争向前的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 确保实际线程数不超过 TOTAL_VILLAGES
num_workers = min(NUM_WORKERS, TOTAL_VILLAGES)
# 向上取整计算每个线程处理的区间大小
block_size = (TOTAL_VILLAGES + num_workers - 1) // num_workers

# 使用 tqdm 显示总进度
pbar = tqdm(total=TOTAL_VILLAGES, desc="", ncols=80)

with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
futures = []
for block in range(num_workers):
start_id = block * block_size + 1
end_id = min((block + 1) * block_size, TOTAL_VILLAGES)
futures.append(
executor.submit(process_village_range, start_id, end_id, proxy_pool, user_agent, progress, progress_lock, pbar)
)
concurrent.futures.wait(futures)

pbar.close()

然后就可以开爬了。


4. 解析RAW数据

上文爬虫是直接保存了返回结果为 json 文件,现在来对 json 解析为 md 文件。

  • 返回结果 json 的格式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
{
"code": 200,
"data": {
"provinceName": "河南省",
"cityName": "洛阳市",
"countyName": "孟津区",
"townName": "会盟镇",
"villageName": "双槐村",
"createTime": "2022年08月04日",
"types": [
{
"id": 5879,
"scoreRuleId": 761,
"scoreTypeDetail": "乡村文明",
"villageId": 1285,
"enable": 0,
"createBy": "",
"createTime": "2022-08-04 15:32:46",
"updateBy": "",
"updateTime": "2022-08-04 15:32:46",
"sortNumber": "",
"details": [
{
"id": 42997,
"scoreTypeId": 5879,
"scoreDetail": "生活费",
"villageId": 1285,
"enable": 0,
"createBy": "",
"createTime": "2022-08-04 15:32:46",
"updateBy": "",
"updateTime": "2022-08-04 15:32:46",
"sortNumber": "",
"isUpdate": false,
"auditStatus": 1,
"auditText": "",
"isThumbUp": false,
"thumbUpNumber": 0,
"avatarPath": "",
"volunteerFlag": "",
"isParty": "",
"residentPost": "",
"updateUserName": "",
"avatarFrame": "",
"userExtraInfo": ""
}
],
"isUpdate": "",
"auditStatus": 1,
"auditText": "",
"total": "",
"pages": ""
}
],
"isTmp": "",
"auditRunningCount": 0,
"auditRefuseCount": 0,
"id": "",
"revisionDate": "",
"typeDetailTotal": "共1章1条",
"thumbUpNumber": 0,
"viewNum": 9
},
"requestId": "",
"message": "请求成功"
}
  • 我希望得到的 md 文件格式:
1
2
3
4
5
6
7
8
9
10
11
---
title: <villageName>积分规则
updateTime: <updateTime>
downloadTime: <从`progress`中获取>
path: <provinceName>-<cityName>-<countyName>-<townName>-<villageName>
---

## scoreTypeDetail
- scoreDetail
- scoreDetail
...

5. 分析结果

断断续续、缝缝补补爬了 10 天,下面分析一下结果

5.1 爬虫结果

status_pie

100w 个 id 中爬到数据的只有大概 10%,1w 条左右。

5.2 正文字数

对解析得到的 md 文件进行正文字数的分析:

Statistic Value
count 11501
mean 3722.031302
std 3767.899372
min 5
25% 766
50% 1967
75% 7246
max 66384

看起来还挺离散的分布,而且最大字数去到了 6.6w 、最小字数只有 5,有点离谱。

word_count_distribution

从柱状图看主要聚集在 3k 以下和 7k-8k 两个区间。

5.3 更新时间

我突发奇想分析了一下这些积分规则的更新时间

time_series

calendar_heatmap_stacked

可以看到最后更新在 2023 年中旬左右的规则最多,至今也有两年多了。

而且也可以明显看到基本都是工作日最后更新哈哈😂。

5.4 爬虫代理使用情况

这个其实和项目本身没什么关系,单纯统计了一下:

country_distribution

country_pie

爬虫大部份还是使用的国内代理,墨西哥、印度尼西亚、美国次之。