在店鋪詳情頁下拉 點擊更多點評 纔會進入真正的詳情頁。
在這個頁面,使用的是css的文字映射反爬。
分析一下頁面 可以看到部分評論的數據是缺失的,跟每個節點的class屬性可能有關係
可以打開這個文件,找到節點class屬性 映射的像素值。這個url可以用正則找到
svg_text_url = re.findall('<link rel="stylesheet" type="text/css" href="(.*?svg.*?)">', html)[0] #獲得節點名對應座標 css文件地址
可以看到 css文件中 都是每個節點的class屬性 對應的地址。接下來我們要找到地址所對應的文字
**
在css文件中,有一些 包含svg的url 將它打開
這個就是文字表,可以根據css座標提取出相應的文字。
注意:有三個不同的文字座標文件。其中只有兩個座標文件有用,點評網會在不同時間使用這兩個座標文件。如果進行長時間採集,需要對這兩個座標文件進行判斷,來選擇不同的座標文件進行使用。
接下來我們的思路已經確定了
1.請求詳情頁數據,使用正則表達式找到css文件地址。
2.在css文件中 使用正則提取出所有節點名稱以及對應的座標。並找到文字座標文件svg的地址
3.根據座標 提取出.svg文件中對應的文字。形成節點名稱對應文字的字典。
4.將請求詳情頁中返回的html,根據節點class屬性替換爲對應的文字。
5.正常解析html即可
在.svg文件根據座標提取文字時,不同的時間段,點評網會使用不同的.svg文件進行匹配。
當.svg文件內容如下時 匹配規則如下:
例:
在html中 “午”字 對應的節點class屬性爲 “xucya”
“xucya” 對應的座標爲 “-14 -959”
其中 -959確定文字所在行 -14確定文字橫向偏移量
可以看到svg返回數據中,每行的y值爲一個個區間,根據959數字的大小確定區間,即可確定所在行。
確定行之後,橫向便宜量爲 數字/14 以14爲例 14/14=1 則橫向偏移量爲1,確定的文字爲當前行的第二個字。
當.svg文件內容如下時 匹配規則如下:
將每個節點前的 #數字 替換爲上方的值 如下圖所示
替換完成後 匹配規則和上面svg文件的匹配規則相同。
注:不可能每次啓動採集都手動修改匹配方式,我們需要對svg文件做一個判斷,完成自動化的匹配。可以根據svg內容信息來判斷,如行數。每次解析svg文件,若行數小於20,則跳出繼續解析下一個svg文件。如下圖
相關代碼:
def get_node_dict(css_url, cookie):
"""
獲取座標值 對應 文字 字典
:param background_image_link:
:return:
"""
res = requests.get(css_url, headers=css_headers)
node_data_ls = re.findall(r'\.([a-zA-Z0-9]{5,6}).*?round:(.*?)px (.*?)px;', res.text) # 提取節點名與對應座標
background_image_link = re.findall("background-image: url\((.*?)\)", res.text) # 提取 座標對應數字 css文件地址
word_coordinate_dict, y_ls = get_word_coordinate_dict(background_image_link) #
node_data_dict = {}
for i in node_data_ls:
"""構造成{節點名: 數字, .........}"""
x = -int(i[1][:-2]) // 14
for index in range(len(y_ls)):
if -int(i[2][:-2]) <= int(y_ls[index]):
y = y_ls[index]
break
try:
node_data_dict[i[0]] = word_coordinate_dict[(int(x), str(y))]
except Exception as e:
pass
return node_data_dict
def get_word_coordinate_dict(background_image_link):
"""
獲取座標值 對應 文字 字典
:param background_image_link:
:return:
"""
word_coordinate_dict = {}
y_ls = []
for svg_url in background_image_link:
url = "http:" + svg_url
res = requests.get(url, headers=css_headers)
# time.sleep(random.uniform(30, 120)) # 隨機休眠
Text = res.text
if 'x=' in Text:
font_list = re.findall(r'<text x="(.*?)" y="(.*?)">(.*?)</text>', Text) # 提取座標對應數字
if len(font_list) < 20:
continue
for i in font_list:
if i[1] not in y_ls:
y_ls.append(i[1])
for j in range(len(i[2])):
word_coordinate_dict[(j, i[1])] = i[2][j]
elif 'textPath' in Text:
Y_ls = re.findall(r'<path id="(.*?)" d="M0 (.*?) H600"/>', Text)
if len(Y_ls) < 20:
continue
font_list = re.findall(r'<textPath xlink:href="(.*?)" textLength="(.*?)">(.*?)</textPath>', Text)
font_list = [(i[1], j[1], i[2]) for i, j in zip(font_list, Y_ls)]
for i in font_list:
if i[1] not in y_ls:
y_ls.append(i[1])
for j in range(len(i[2])):
word_coordinate_dict[(j, i[1])] = i[2][j]
return word_coordinate_dict, y_ls
def replace_html(html, css_url, cookie):
"""
提取全部節點,根據節點名 替換數據 返回真實html數據
:param html:
:param css_url:
:param cookie:
:return:
"""
node_data_dict = get_node_dict(css_url, cookie)
node_names = set()
for i in re.findall('<svgmtsi class="([a-zA-Z0-9]{5,6})"></svgmtsi>', html): # 提取所有節點名
node_names.add(i)
for node_name in node_names:
try:
html = re.sub('<svgmtsi class="%s"></svgmtsi>' % node_name, node_data_dict[node_name], html) # 替換html節點爲數字
except KeyError as e:
# print(e)
pass
return html
def parse_html(html, shop_id):
"""
解析html 第一次採集 提取好評差評數量
後續採集 提取口味,環境,服務,食材,星級,評論
:param html:
:param i:
:return:
"""
sel = etree.HTML(html)
save_data = []
for node_num in range(1, 16):
comment_content_3 = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[3]/text()'.format(node_num)) # 評論數據存在於 兩個節點 div3號節點數據
comment_content_4 = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[4]/text()'.format(node_num)) # div4號節點數據
if len(''.join(comment_content_3)) > len(''.join(comment_content_4).replace(' ', '').replace(r'\n', '')):
comment_content = comment_content_3
else:
comment_content = comment_content_4
taste_score = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[2]/span[2]/span[1]/text()'.format(node_num)) # 口味評分
environment_score = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[2]/span[2]/span[2]/text()'.format(node_num)) # 環境評分
service_score = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[2]/span[2]/span[3]/text()'.format(node_num)) # 服務評分
food_score = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[2]/span[2]/span[4]/text()'.format(node_num)) # 食物評分
food_score = ''.join(food_score).strip().replace('食材:', '')
per_capita = ''
if "人均" in food_score: #
per_capita = food_score.replace('人均:', '').replace('元', '')
food_score = ''
star_level = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[3]/div[3]/ul/li[{}]/div/div[2]/span[1]/@class'.format(node_num))
item = {
'taste_score': ''.join(taste_score).strip().replace('口味:', ''),
'environment_score': ''.join(environment_score).strip().replace('環境:', ''),
'service_score': ''.join(service_score).strip().replace('服務:', ''),
'food_score': food_score,
'per_capita': per_capita,
'shop_id': shop_id,
'star_level': ''.join(star_level).strip().replace('sml-rank-stars sml-str', '').replace(' star', ''),
'comment_content': ''.join(comment_content).strip(),
}
save_data.append(item)
return save_data
def parse_action(cookie, main_url, proxy, Thread_name, progress):
"""
詳情頁採集主函數
:param cookie:
:param url:
:return:
"""
# cookie = "__mta=51347094.1571707475917.1571707475917.1571707475917.1; _lxsdk_cuid=16ddc82c62cc8-0c2da24cdecceb-7373e61-15f900-16ddc82c62dc8; _lxsdk=16ddc82c62cc8-0c2da24cdecceb-7373e61-15f900-16ddc82c62dc8; _hc.v=63c163cf-d75d-89b9-0dc1-74a9b2026548.1571362621; ua=sph; ctu=cdff7056daa2405159990763801b14e0b74443eb8302ce0928ea5e3d95696905; s_ViewType=10; aburl=1; _dp.ac.v=da84abba-5c47-4040-ac30-40e09e04162f; uamo=15292060685; cy=1; cye=shanghai; dper=ae518422253841cb8382badd9f84a3d471e237b43479f8d74f098a5ea02eabda1d22f0912bc506af3ff0f9dfc1cabd260be8824e9a0abeff2b67e50524f5875806e64becebd3923d9b402b395373f85a7f4228bc2e2dfc6533237b19446496e3; ll=7fd06e815b796be3df069dec7836c3df; _lx_utm=utm_source%3DBaidu%26utm_medium%3Dorganic; _lxsdk_s=16e353fda1a-7ba-0ce-cef%7C%7C124"
# url = "http://www.dianping.com/shop/128001304/review_all"
headers["Cookie"] = cookie
headers['Host'] = 'www.dianping.com'
# ip_list = requests.get(
# url="http://route.xiongmaodaili.com/xiongmao-web/api/glip?secret=8c1fe70d7ceb3a4e77284561df11f0d5&orderNo=GL20191111103706QQa4ktHu&count=1&isTxt=1&proxyType=1").text.split("\r\n")
# ip_pool = [{"http": "http://{}".format(ip)} for ip in ip_list if ip != ""]
# main_url = "http://www.dianping.com/shop/100034705"
shop_id = main_url.replace("http://www.dianping.com/shop/", "") # 提取shop_id
if progress == None:
progress = 1
for page_num in range(int(progress)+1, 1000):
url = "http://www.dianping.com/shop/{}/review_all/p{}?queryType=isAll&queryVal=true".format(shop_id, page_num) # 生成url
time.sleep(random.uniform(85, 120)) # 每次隨機休眠50-100秒
i = 0
while i < 2:
try:
rep = requests.get(url, headers=headers, timeout=25, proxies=proxy)
break
except requests.exceptions.ConnectionError as e:
i += 1
time.sleep(300)
print('{}: 請求失敗 休眠300秒 url:{}'.format(Thread_name, url))
continue
Text = rep.text
sel = etree.HTML(Text)
go_on_flag = sel.xpath('//*[@id="review-list"]/div[2]/div[3]/div[2]/div[3]/text()')
if go_on_flag == ["暫無點評"]: # 解析出暫無電影 則退出採集
print("{}: {}店鋪 採集完畢".format(Thread_name, shop_id, ))
Mysql.modify_statue(main_url, page_num, 1)
break
css_url = "http:" + re.findall('<link rel="stylesheet" type="text/css" href="(.*?svg.*?)">', Text)[0] # 提取cssurl地址
html = replace_html(Text, css_url, cookie) # 原始html 替換節點後 生成帶有原文的html
save_data = parse_html(html, shop_id) # 解析
Mysql.save_comment(url, save_data, Thread_name) # 存儲
Mysql.modify_statue(main_url, page_num)
項目github:https://github.com/sph116/dazhong_spider_font_svg