进入京东(https://www.jd.com)后,我如果搜索特定的手机产品,如oppo find x2,会先出现如下的商品列表页:
如果点击进入其中一个商品会进入到如下图所示的商品详情页,可以看到用户对该商品的评论:
这篇博客主要是记录我怎么爬取商品列表页和详情页,我使用Selenium,模拟浏览器实现自动化的用户浏览操作,能在一定程度上规避反爬虫(爬取平台对你进行屏蔽操作)的风险。总体来说,列表页和详情页的爬取数据过程中用户模拟行为如下所示:
一、商品列表页数据爬取
首先,先介绍爬取列表页所需要的实现的功能:
1. 设置模拟浏览器
def open_browser(self):
# 若下列命令报错,请进入下面链接下载chromedriver然后放置在/user/bin/下即可
# https://chromedriver.storage.googleapis.com/index.html?path=2.35/
self.options = webdriver.ChromeOptions()
self.options.add_argument(self.user_agent)
self.browser = webdriver.Chrome(options = self.options)
# 隐式等待:等待页面全部元素加载完成(即页面加载圆圈不再转后),才会执行下一句,如果超过设置时间则抛出异常
try:
self.browser.implicitly_wait(10)
except:
print("页面无法加载完成,无法开启爬虫操作!")
# 显式等待:设置浏览器最长允许超时的时间
self.wait = WebDriverWait(self.browser, 10)
上述代码解释:
2. 初始化参数
def init_variable(self, url_link, search_key, user_agent):
# url_link为电商平台首页,search_key为商品搜索词
self.url = url_link
self.keyword = search_key
self.isLastPage = False
self.user_agent = user_agent
上述参数分别是:
3. 解析并爬取列表单页内容
def parse_JDpage(self):
try:
# 定位元素并获取元素下的字段值(商品标题,价格,评论数,商品链接)
names = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, '//div[@class="gl-i-wrap"]/div[@class="p-name p-name-type-2"]/a/em')))
prices = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, '//div[@class="gl-i-wrap"]/div[@class="p-price"]/strong/i')))
comment_nums = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, '//div[@class="gl-i-wrap"]/div[@class="p-commit"]/strong')))
links = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, '//li[@class="gl-item"]')))
page_num = self.wait.until(EC.presence_of_element_located((By.XPATH, '//div[@class="page clearfix"]//input[@class="input-txt"]')))
names = [item.text for item in names]
prices = [price.text for price in prices]
comment_nums = [comment_num.text for comment_num in comment_nums]
links = ["https://item.jd.com/{sku}.html".format(sku = link.get_attribute("data-sku")) for link in links]
page_num = page_num.get_attribute('value')
except selenium.common.exceptions.TimeoutException:
print('parse_page: TimeoutException 网页超时')
self.parse_JDpage()
except selenium.common.exceptions.StaleElementReferenceException:
print('turn_page: StaleElementReferenceException 某元素因JS刷新已过时没出现在页面中')
print('刷新并重新解析网页…')
self.browser.refresh()
self.parse_JDpage()
print('解析成功')
return names, prices, comment_nums, links, page_num
上述代码解释如下:
4. 翻页
def turn_JDpage(self):
# 移到页面末端并点击‘下一页’
try:
self.browser.find_element_by_xpath('//a[@class="pn-next" and @onclick]').click()
time.sleep(1) # 点击完等1s
self.browser.execute_script("window.scrollTo(0, document.body.scrollHeight)")
time.sleep(2) # 下拉后等2s
# 如果找不到元素,说明已到最后一页
except selenium.common.exceptions.NoSuchElementException:
self.isLastPage = True
# 如果页面超时,跳过此页
except selenium.common.exceptions.TimeoutException:
print('turn_page: TimeoutException 网页超时')
self.turn_JDpage()
# 如果因为JS刷新找不到元素,重新刷新
except selenium.common.exceptions.StaleElementReferenceException:
print('turn_page: StaleElementReferenceException 某元素因JS刷新已过时没出现在页面中')
print('刷新并重新翻页网页…')
self.browser.refresh()
self.turn_JDpage()
print('翻页成功')
上述代码解释:
5. 自动化爬取多页
def JDcrawl(self, url_link, search_key, user_agent, save_path):
# 初始化参数
self.init_variable(url_link, search_key, user_agent)
df_names = []
df_prices = []
df_comment_nums = []
df_links = []
# 打开模拟浏览器
self.open\_browser()
# 进入目标JD网站
self.browser.get(self.url)
# 在浏览器输入目标商品名,然后搜索
self.browser.find\_element\_by\_id('key').send\_keys(self.keyword)
self.browser.find\_element\_by\_class\_name('button').click()
# 开始爬取
self.browser.execute\_script("window.scrollTo(0, document.body.scrollHeight)")
print("################\\n##开启数据爬虫##\\n################\\n")
while self.isLastPage != True:
page\_num = 0
names, prices, comment\_nums, links, page\_num = self.parse\_JDpage()
print("已爬取完第%s页" % page\_num)
df\_names.extend(names)
df\_prices.extend(prices)
df\_comment\_nums.extend(comment\_nums)
df\_links.extend(links)
self.turn\_JDpage()
# 退出浏览器
self.browser.quit()
# 保存结果
results = pd.DataFrame({'title':df\_names,
'price':df\_prices,
'comment\_num':df\_comment\_nums,
'url':df\_links})
results.to\_csv(save\_path, index = False)
print("爬虫全部结束,共%d条数据,最终结果保存至%s" % (len(results),save\_path))
以上代码就是将之前模块组合在一起,这里不过多进行过多解释。
二、商品详情页数据爬取
当我们爬取完列表页数据后,可以通过依次进入各个商品详情页爬取用户评论内容。虽然大体流程跟列表页爬取流程差不多,但具体爬取过程的细节上还是有些不同的处理方法。
1. 清洗列表页数据
因为爬取商品列表中的商品很多不是我们真正想要的爬取的内容,比如:我们不想看二手拍拍的商品,只想看oppo find x2而不适它的pro版本,另外也不想看oppo find x2的相关配件商品。所以,我们要对列表页数据执行清洗过程:(1) 不要评论少的商品;(2) 不要商品名称带‘拍拍’标签的商品;(3) 不要部分匹配搜索词的商品;(4) 不要价格过低的商品。清洗代码如下:
def clean_overview(self, csv_path, search_keyword):
'''
清洗数据
1. 清洗掉少于11条评论的数据
2. 清洗掉含‘拍拍’关键词的数据
3. 清洗掉不含搜索关键词的数据
4. 清洗掉价格过低的数据
输入:
1. csv\_path (str): 爬取的overview商品展示页下的结果文件路径。
2. search\_keyword (str): 爬取的overview商品展示页下搜索关键词。
输出:
1. overview\_df (pd.DataFrame): 清洗好的overview数据
'''
overview\_path = csv\_path
overview\_df = pd.read\_csv(overview\_path)
search\_key = search\_keyword
# 1. 清洗掉少于11条评论的数据
print("原始数据量:%s" % len(overview\_df))
drop\_idxs = \[\]
comment\_nums = overview\_df\['comment\_num'\]
for i, comment\_num in enumerate(comment\_nums):
try:
if int(comment\_num\[:-3\]) in list(range(11)):
drop\_idxs.append(i)
except:
pass
print("清洗掉少于11条评论的数据后的数据量:%s(%s-%s)" % (len(overview\_df) - len(drop\_idxs), len(overview\_df), len(drop\_idxs)))
overview\_df.drop(drop\_idxs, axis = 0, inplace = True)
overview\_df = overview\_df.reset\_index(drop = True)
# 2. 清洗掉含‘拍拍’关键词的数据
drop\_idxs = \[\]
comment\_titles = overview\_df\['title'\]
for i, title in enumerate(comment\_titles):
try:
if title.startswith('拍拍'):
drop\_idxs.append(i)
except:
pass
print("清洗掉含‘拍拍’关键词的数据后的数据量:%s(%s-%s)" % (len(overview\_df) - len(drop\_idxs), len(overview\_df), len(drop\_idxs)))
overview\_df.drop(drop\_idxs, axis = 0, inplace = True)
overview\_df = overview\_df.reset\_index(drop = True)
# 3. 清洗掉不含搜索关键词的数据
drop\_idxs = \[\]
comment\_titles = overview\_df\['title'\]
for i, title in enumerate(comment\_titles):
if search\_key.replace(" ","") not in title.lower().replace(" ",""):
drop\_idxs.append(i)
print("清洗掉不含搜索关键词的数据后的数据量:%s(%s-%s)" % (len(overview\_df) - len(drop\_idxs), len(overview\_df), len(drop\_idxs)))
overview\_df.drop(drop\_idxs, axis = 0, inplace = True)
overview\_df = overview\_df.reset\_index(drop = True)
# 4. 清洗掉价格过低/过高的数据
drop\_idxs = \[\]
comment\_prices = overview\_df\['price'\]
prices\_df = {}
for p in comment\_prices:
if p not in list(prices\_df.keys()):
prices\_df\[p\] = 1
else:
prices\_df\[p\] += 1
# print("各价格下的商品数:", prices\_df)
# {4499: 89, 5999: 5, 6099: 1, 12999: 2, 6999: 1, 89: 1, 29: 1}
# 通过上述结果,我们只要价位为4499的商品结果即可了
for i, p in enumerate(comment\_prices):
if p != 4499.0:
drop\_idxs.append(i)
print("清洗掉价格过低/过高的数据后的数据量:%s(%s-%s)" % (len(overview\_df) - len(drop\_idxs), len(overview\_df), len(drop\_idxs)))
overview\_df.drop(drop\_idxs, axis = 0, inplace = True)
overview\_df = overview\_df.reset\_index(drop = True)
return overview\_df
2. 设置模拟浏览器
同样地,我们先进行浏览器设置。注意:在这里我们的隐式等待和显式等待都增加了,这里的原因是因为京东对详情页的爬取加重防守了,所以我们得给页面加载和元素定位更多的等待和尝试时间,不然会什么都爬不到。
def open_browser(self):
'''设置浏览器'''
# 若下列命令报错,请进入下面链接下载chromedriver然后放置在/user/bin/下即可
# https://chromedriver.storage.googleapis.com/index.html?path=2.35/
self.options = webdriver.ChromeOptions()
self.browser = webdriver.Chrome(options = self.options)
# 隐式等待:等待页面全部元素加载完成(即页面加载圆圈不再转后),才会执行下一句,如果超过设置时间则抛出异常
try:
self.browser.implicitly_wait(50)
except:
print("页面无法加载完成,无法开启爬虫操作!")
# 显式等待:设置浏览器最长允许超时的时间
self.wait = WebDriverWait(self.browser, 30)
3. 初始化参数
这里增加了两个变量 isLastPage 和 ignore_page ,因为京东会自动折叠掉用户默认评价(即忽略评价),如果点击查看忽略评价会蹦出新的评论窗口,所以后续代码需要这两个变量帮助爬虫正常进行。
def init_variable(self, csv_path, search_key, user_agent):
'''初始化变量'''
self.csv_path = csv_path # 商品总览页爬取结果文件路径
self.keyword = search_key # 商品搜索关键词
self.isLastPage = False # 是否为页末
self.ignore_page = False # 是否进入到忽略评论页面
self.user_agent = user_agent # 用户代理,这里暂不用它
4. 解析并爬取详情单页内容
def parse_JDpage(self):
try:
time.sleep(10) # 下拉后等10s
# 定位元素(用户名,用户等级,用户评分,用户评论,评论创建时间,购买选择,页码)
user_names = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, '//div[@class="user-info"]')))
user_levels = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, '//div[@class="user-level"]')))
user_stars = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, '//div[@class="comment-column J-comment-column"]/div[starts-with(@class, "comment-star")]')))
comments = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, '//div[@class="comment-column J-comment-column"]/p[@class="comment-con"]')))
order_infos = self.wait.until(EC.presence_of_all_elements_located((By.XPATH, '//div[@class="comment-item"]//div[@class="order-info"]')))
if self.ignore_page == False:
# 如果没进入忽略页
page_num = self.wait.until(EC.presence_of_element_located((By.XPATH, '//a[@class="ui-page-curr"]')))
else:
# 如果进入忽略页
page_num = self.wait.until(EC.presence_of_element_located((By.XPATH, '//div[@class="ui-dialog-content"]//a[@class="ui-page-curr"]')))
# 获取元素下的字段值
user_names = [user_name.text for user_name in user_names]
user_levels = [user_level.text for user_level in user_levels]
user_stars = [user_star.get_attribute('class')[-1] for user_star in user_stars]
create_times = [" ".join(order_infos[0].text.split(" ")[-2:]) for order_info in order_infos]
order_infos = [" ".join(order_infos[0].text.split(" ")[:-2]) for order_info in order_infos]
comments = [comment.text for comment in comments]
page_num = page_num.text
except selenium.common.exceptions.TimeoutException:
print('parse_page: TimeoutException 网页超时')
self.browser.refresh()
self.browser.find_element_by_xpath('//li[@data-tab="trigger" and @data-anchor="#comment"]').click()
time.sleep(30)
user_names, user_levels, user_stars, comments, create_times, order_infos, page_num = self.parse_JDpage()
except selenium.common.exceptions.StaleElementReferenceException:
print('turn_page: StaleElementReferenceException 某元素因JS刷新已过时没出现在页面中')
user_names, user_levels, user_stars, comments, create_times, order_infos, page_num = self.parse_JDpage()
return user_names, user_levels, user_stars, comments, create_times, order_infos, page_num
上述代码解释:
5. 翻页
def turn_JDpage(self):
# 移到页面末端并点击‘下一页’
try:
if self.ignore_page == False:
self.browser.find_element_by_xpath('//a[@class="ui-pager-next" and @clstag]').send_keys(Keys.ENTER)
else:
self.browser.find_element_by_xpath('//a[@class="ui-pager-next" and @href="#none"]').send_keys(Keys.ENTER)
time.sleep(3) # 点击完等3s
self.browser.execute_script("window.scrollTo(0, document.body.scrollHeight)")
time.sleep(5) # 下拉后等5s
# 如果找不到元素
except selenium.common.exceptions.NoSuchElementException:
if self.ignore_page == False:
try:
# 如果有忽略评论的页面但没进入,则进入继续翻页
self.browser.find_element_by_xpath('//div[@class="comment-more J-fold-comment hide"]/a').send_keys(Keys.ENTER)
self.ignore_page = True
print("有忽略评论的页面")
except:
# 如果没有忽略评论的页面且最后一页
print("没有忽略评论的页面")
self.ignore_page = True
self.isLastPage = True
else:
# 如果有忽略评论的页面且到了最后一页
print("没有忽略评论的页面")
self.isLastPage = True
except selenium.common.exceptions.TimeoutException:
print('turn_page: TimeoutException 网页超时')
time.sleep(30)
self.turn_JDpage()
# 如果因为JS刷新找不到元素,重新刷新
except selenium.common.exceptions.StaleElementReferenceException:
print('turn_page: StaleElementReferenceException 某元素因JS刷新已过时没出现在页面中')
self.turn_JDpage()
需要注意的是:
6. 自动化爬取多页
def JDcrawl_detail(self, csv_path, search_key, user_agent):
# 初始化参数
self.init_variable(csv_path, search_key, user_agent)
unfinish_crawls = 0 # 记录因反爬虫而没有完全爬取的商品数
# 清洗数据
self.overview_df = self.clean_overview(self.csv_path, self.keyword)
# 依次进入到单独的商品链接里去
for url in tqdm(list(self.overview\_df\['url'\]\[3:\])):
df\_user\_names = \[\]
df\_user\_levels = \[\]
df\_user\_stars = \[\]
df\_comments = \[\]
df\_create\_times = \[\]
df\_order\_infos = \[\]
# 打开模拟浏览器
self.open\_browser()
# 进入目标网站
self.browser.get(url)
time.sleep(35)
# 进入评论区
self.browser.find\_element\_by\_xpath('//li\[@data-tab="trigger" and @data-anchor="#comment"\]').click()
time.sleep(15)
# 开始爬取
self.browser.execute\_script("window.scrollTo(0, document.body.scrollHeight)")
self.isLastPage = False
self.ignore\_page = False
self.lastpage = 0
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + " 开启数据爬虫url:",url)
while self.isLastPage != True:
page\_num = 0
user\_names, user\_levels, user\_stars, comments, create\_times, order\_infos, page\_num = self.parse\_JDpage()
# 如果某页因为反爬虫无法定位到‘下一页’元素,导致重复爬取同一页,则保留之前的爬取内容,然后就不继续爬这个商品了
if self.lastpage != page\_num:
self.lastpage = page\_num
print("已爬取完第%s页" % page\_num)
df\_user\_names.extend(user\_names)
df\_user\_levels.extend(user\_levels)
df\_user\_stars.extend(user\_stars)
df\_comments.extend(comments)
df\_create\_times.extend(create\_times)
df\_order\_infos.extend(order\_infos)
self.turn\_JDpage()
else:
unfinish\_crawls += 1
self.browser.quit()
break
# 退出浏览器
self.browser.quit()
# 保存结果
results = pd.DataFrame({'user\_names':df\_user\_names,
'user\_levels':df\_user\_levels,
'user\_stars':df\_user\_stars,
'omments':df\_comments,
'create\_times':df\_create\_times,
'order\_infos':df\_order\_infos})
url\_id = url.split('/')\[-1\].split('.')\[0\]
save\_path = r'/media/alvinai/Documents/comment\_crawler/JD\_results/Detail/' + str(url\_id) + '.csv'
results.to\_csv(save\_path, index = False)
print("爬虫结束,共%d条数据,结果保存至%s" % (len(results),save\_path))
与列表页爬取不同在于,这里是爬完一个商品详情就保存它的结果文件,避免‘一损具损’的局面出现。另外等待时间也给多了(避免京东反爬虫)。
给大家展示下爬取界面:
我的部分爬取结果如下:
三、总结
1. 京东对列表页的爬虫的防守没有详情页严格,所以在对详情页的时候要增加等待时间并且增加一些针对反爬虫的操作。
2. 在我爬虫详情页的过程中,京东的反爬虫有以下两个体现:(1) 点击进入评论区后,不给我显示评论(解决办法:刷新浏览器重新进入评论区);(2) 评论区不给我显示‘下一页’按钮,这样我就没法定位并点击它实现翻页(暂无完美解决方法,减少这种情况发生的手段是增加进入评论区和爬去内容过程中强制等待的时间,后期可以考虑记录下这些网页并重新爬取)。
3. 在我上面的代码中,我增加了对忽略评论的爬取,原因是:尽管评论主体的文本内容没有价值,但是用户们的购买选择是具有价值的,通过这些购买选择,我们后面能进行用户对于机型、颜色、内存搭配、套餐选择的偏好分析。
4. 这是个人下班之余做的自我学习项目,与工作内容无关。
5. 我的代码落地性不强,原因是爬虫速度慢,这也为了考虑到是个人项目,如果要速度上去,不仅可能要考虑多线程,还有购买代理ip等操作,那样的话,就等于完全深入到爬虫里面了,我不是我的目的,因为我只想要它的数据。而且真要落地那种爬虫,代码再优雅而离不开行为的‘野蛮’,对爬虫平台造成大的服务器负担反而不好。从爬虫平台去考虑,他们要区分你是爬虫还是正常浏览,一个常见的思路就是你的浏览行为是否符合真实用户的浏览行为,这也是我为什么我的爬虫等待时间比较长,因为只要你不增加平台服务器负担,他们还是会‘让‘着点你的。
6. 这些数据有什么用?有用,比如说(1)如上面第3点所说,可以进行自身或竞品分析,从出售的机型版本、颜色、内存、套装选择、价格等进行统计性分析和对比; (2) 文本数据的观点抽取,标签生成,情感分析等,用以进行痛点和卖点的挖掘。这也是我接下来会想继续尝试的方向。
7. 完整代码已开源至:https://github.com/AlvinAi96/COI
手机扫一扫
移动阅读更方便
你可能感兴趣的文章