1-起因
在3月下旬,领导突然组织人力登记各省市的收费站坐标。文档共9000+条数据,不仅要确认收费站名称,所在省市区,还要在规定网站查询其对应的经纬度并粘贴下来。这任务砸在谁头上都是一座大山,手头有空闲的我,就成了愚公移山的一员。
2-确定任务
拿到任务后我首先做的是查看查询网站的情况,其网址如下:https://lbs.qq.com/tool/getpoint/index.html
网站为腾讯位置服务,不需登录,即填即查,但是需要选定被查询收费站的所属城市,否则页面不显示坐标信息。
但在实际操作过程中我发现,在搜索框填入收费站名称时,虽然当前城市与其不相符,搜索下拉框还是会出现被搜索收费站的正确名称。这让人怀疑,后台已经搜索到了对应数据,但前台有相关限制导致其不显示。于是我使用fiddler和google开发者工具,打算看看它的后台构造。
使用开发者工具可看出,它在展示页面 lbs.qq.com/getpoint.html 后套了一个实际查询网址 apis.map.qq.com ,见图-嵌套查询网址,而这个网址搜索时会有两个请求分支suggestion和search,请求suggestion分支时,即使城市与收费站不相符,也会获得相应坐标。见图-城市与收费站不相符
虽然使用google开发者工具已经有了结果,为了测试该查询网址是否可靠,我使用fiddler_composer对请求进行修改后反复尝试,确认了返回信息的可靠性。图中测试收费站为济南东收费站,城市为北京,可正确返回收费站坐标。见图-composer测试1、composer测试2
3-代码实现
在具体请求和响应已知的情况下,我决定使用python的requests库编写爬虫爬取收费站信息。
具体代码如下:
import requests
import json, re
import xlrd,xlwt
*导入excel数据*
region = {'那曲'}
wb = xlrd.open_workbook("C:\\Users\\why\\Desktop\\test_io.xlsx")
sheet3 = wb.sheet_by_index(2)
nrows = sheet3.nrows
ncols = sheet3.ncols
*创建导出excel文件*
workbook = xlwt.Workbook(encoding = 'utf-8')
worksheet = workbook.add_sheet("test Output",cell_overwrite_ok=True)
*建立表头*
worksheet.write(0,0,"待查询收费站")
worksheet.write(0,1,"查询结果")
worksheet.write(0,2,"地址")
worksheet.write(0,3,"纬度")
worksheet.write(0,4,"经度")
*遍历收费站*
*从excel文件中导入数据*
for rows in range(1,nrows):
value = sheet3.cell_value(rows,0)
worksheet.write(rows, 0, value)
key = value[:-1]+"收费站"
print(key)
*坐标查询网址的构建使用format*
url_q = "https://apis.map.qq.com/ws/place/v1/suggestion/?keyword={0}®ion=({1},0)&key=K76BZ-W3O2Q-RFL5S-GXOPR-3ARIT-6KFE5&output=jsonp&&callback=jQuery19109172698889312632_1586930162787&_=1586930162806".format(key,region)
headers = {
'Host': 'apis.map.qq.com',
'Connection': 'keep-alive',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36',
'Sec-Fetch-Dest': 'script',
'Sec-Fetch-Site': 'same-site',
'Sec-Fetch-Mode': 'no-cors',
'Referer': 'https://lbs.qq.com/tool/getpoint/getpoint.html',
'Cookie': 'mpuv=0dbf808f-b2b9-4a52-cc5c-7f02c7624209; pgv_pvid=8146180802; pgv_pvi=6788941824; pac_uid=0_5d4ce4f3a45e9; eas_sid=l185a6s62976r029G632j169B8; Qs_lvt_323937=1569721323; Qs_pv_323937=4182702269183565000; _ga=GA1.2.1124364354.1569721323; ptcz=1a2f9854c35dd273dbe54d8349cceedee3d4fb18c41cb1efa48538c355478cff; RK=JYr0uKKMGI; tvfe_boss_uuid=128c48d3412095ed; manshenqi=BASE64cXFAQF4oaHR0cDovL2FjXC5xcVwuY29tL0NvbWljVmlldy8oaW5kZXh8Y2hhcHRlcikvaWQvKFswLTldKykvY2lkLyhbMC05XSspKChcP3wjKS4qKT8pJEBAXihodHRwOi8vYWNcLnFxXC5jb20vQ29taWMvQ29taWNJbmZvL2lkLyhbMC05XSspKFw/LiopPykk; XWINDEXGREY=0'
}
*使用session使cookie保持有效*
browser = requests.session()
*进行get请求*
res = browser.get(url=url_q, headers=headers)
jsonp = res.text
*print("获得的jsonp数据格式是:",type(jsonp))*
*使用函数将jsonp格式数据转换为json格式,方便处理*
def loads_jsonp(_jsonp):
"""解析jsonp数据为json格式"""
try:
return json.loads(re.match(".*?({.*}).*",_jsonp,re.S).group(1))
except:
raise ValueError('Invaild Input')
js = loads_jsonp(jsonp) * 将jsonp数据变为dict数据*
if 'data' in js :
list_js = js['data'] *获取收费站数据*
if list_js: * python中{},[],(),都等价于False*
*仅获取查询排名第一的收费站名称、地址、纬度、经度*
print(list_js[0]["title"], ",", list_js[0]["address"], ",", list_js[0]["location"]['lat'], ",", list_js[0]["location"]['lng'])
*导出至excel文件:名称、地址、纬度、经度*
worksheet.write(rows, 1, list_js[0]["title"])
worksheet.write(rows, 2, list_js[0]["address"])
worksheet.write(rows, 3, list_js[0]["location"]['lat'])
worksheet.write(rows, 4, list_js[0]["location"]['lng'])
else:
*在当前数据行的第二列打印Not Found*
worksheet.write(rows,1,"Not Found")
*将数据导出至excel文件*
workbook.save("C:\\Users\\why\\Desktop\\test_out.xls")
print("查询结束")
4、代码解析
4-1 HTTP请求
因为网站经过fiddler抓包后可看到raw的http请求,其中的url,header和相关参数可以直接拿到,这种情况下,我使用python的requests库进行请求编写,因为请求中带有cookie,为保证在程序运行时cookie均有效,我使用了requests的session方法。
其中需要注意的是,该坐标查询网址是由多个参数拼接而成,在查询过程中有两个参数需要变化,一个是keyword(在本次任务中即收费站名称),一个是region(在本次任务中即查询城市)。
keyword可以从xls文件中直接导入,因为待查询的名称没有“收费”两个字,为了提高查询的正确率,我用了简单的字符串截取和添加,将“站”替换为了“收费站”。
region则可以直接固定,尝试后发现,如果在本身已有收费站的城市进行非该城市收费站搜索,搜索结果有一定几率受该城市收费站的影响,因此最后选定没有收费站的西藏“那曲”作为region固定值。
4-2 返回结果处理
在get请求发送后,得到的响应是jsonp格式,例如callbackFunction(["customername1","customername2"])
这个格式我之前没有接触,但网上搜索方案后发现,可以通过去掉首尾的括号,将其变为可处理的数据。因此我使用 网上搜索的loads_jsonp函数将数据进行转换,最终转换为dict格式。
因为最终返回的数据过多且有嵌套,我先取出返回坐标所在的data列表,再从data列表中取出排名第一的数据字典,最终取得相关key的value,导出到xls文件
4-3 xls文件导入导出
因为查询数据所在的文件格式为xls,最终导出也希望保存为xls格式,因此我使用xlrd和xlwt对数据进行导入导出。
具体代码比较简单,都是表明对象使用读/写方法,因为是自己使用,对表格格式没有要求,只是在最初添加了表头,使用的也是简单写方法。
这部分改进为for循环
表头建立代码如下
li = ["待查询收费站","查询结果","地址","纬度","经度"]
for i in range(len(li)):
worksheet.write(0,i,li[i])
数据导出代码如下
lit = [list_js[0]["title"],list_js[0]["address"],list_js[0]["location"]['lat'],list_js[0]["location"]['lng']]
for j in range(len(lit)):
worksheet.write(rows,j+1,lit[j])
5、最终结果
写了这么多,最重要的就是结果。我跑了2313条数据,覆盖9个省的收费站,最终查询数据分析统计见图-查询结果分析
图中可看出,数据平均正确率在77.76%,对于之前低效的人工查询是很有帮助的。
5-1 问题分析
有些省份(如四川),其正确率低至26.82%,需要进行具体分析。
通过观察四川的原查询数据,可看出他的收费站名称均较长见图-四川省原查询数据,除了必要的省份外,还有附加的市区名或道路名,这使系统在查询时大概率返回多个结果,而在代码中,我限制其只输出排名第一的结果,没有对数据进一步验证,因此导致了大量的不符合数据输出。
修改一:原查询数据修改
目前就该问题对代码进行简单,将导入的收费站名称进行字符串截取,保留省份名和收费站名。例如“四川二绕东三星堆站”,保留“四川”和“三星堆”,将“站”替换为“收费站”。
代码如下:
for rows in range(1,nrows):
value = sheet3.cell_value(rows,0)
worksheet.write(rows, 0, value)
key = value[0:2]+value[-4:-1]+"收费站"
或key = value[0:2]+value[-3:-1]+"收费站"
因为一般收费站名称长度为2-3个中文字符,所以我截取了倒数2位和3位,但这样也会出现查询错误。
对之前四川错误数据进行测试,将两次返回内容总计,数据量423,正确率84.40%,四川省正确率至88.58%
统计结果见图-错误数据分析
虽然已有进步,但两次查询后再统计还是非常麻烦,于是继续修改代码
修改二:返回数据处理方式
因为之前的返回数据处理中,我只是简单的把排名第一的数据取出,并没有对其进行正确性验证。因此再次修改时,希望从验证返回数据入手,先验证返回数据中是否存在正确数据,再将正确的数据导出。
代码如下
for num in range(len(list_js)):
* 获得每一条查询数据的名称*
title = list_js[num]["title"]
print(title)
* 验证返回数据是否包含原查询数据一致,一致则导出,否则NF*
if check in title:
print(list_js[num]["title"], ",", list_js[num]["address"], ",", list_js[num]["location"]['lat'], ",",
list_js[num]["location"]['lng'])
lit = [list_js[num]["title"], list_js[num]["address"], list_js[num]["location"]['lat'],
list_js[num]["location"]['lng']]
for j in range(len(lit)):
worksheet.write(rows, j + 1, lit[j])
* 得到正确数据后强制跳出循环*
break
else:
worksheet.write(rows,1,"Not Found")
仍然跑四川错误数据,数据量423,正确率78.96%,四川省正确率至84.60%。
跑这段代码时我截取的是原查询数据倒数两位(即key = value[0:2]+value[-3:-1]+"收费站"),原因有二:一、在上述修改代码中,我发现取后两位的正确率更高;二、虽然后两位和后三位都取才能更好地提高正确率,但是我现在能力有限,如果两种都尝试查询,代码的执行效率不高,现在的办法只能是两种分开查询,再借助excel的选择性粘贴进行统计。
最终结果分析见图-结果分析(总)