目的:按给定关键词爬取京东商品信息,并保存至mongodb。
字段:title、url、store、store_url、item_id、price、comments_count、comments
工具:requests、lxml、pymongo、concurrent
分析:
1. https://search.jd.com/Search?keyword=耳机&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&wq=er%27ji&page=1&s=56&click=0,这是京东搜索耳机的跳转url,其中关键参数为:
keyword:关键词
enc:字符串编码
page:页码,需要注意的是,这里的数值均为奇数
所以简化后的 url 为 https://search.jd.com/Search?keyword=耳机&enc=utf-8&page=1
2. 分析各字段的 xpath,发现在搜索页面只能匹配到 title、url、store、store_url、item_id、price。至于 comments_count、comments 需要单独发出请求。
3. 打开某一商品详情页,点击商品评价,打开开发者工具。点击评论区的下一页,发现在新的请求中,除去响应为媒体格式外,仅多出一个 js 响应,故猜测评论内容包含其中。
4. 分析上述请求的 url,简化后为 https://sclub.jd.com/comment/productPageComments.action?productId=100004325476&score=0&sortType=5&page=0&pageSize=10,其中:
productId:商品的Id,可简单的从详情页的 url 中获取
page:评论页码
5. 由以上可以得出,我们需要先从搜索页面中获取的商品 id,通过 id 信息再去获取评论信息。爬取评论时需要注意,服务器会判断请求头中的 Referer,即只有通过商品详情页访问才能得到评论,所以我们每次都根据 item_id 构造请求头。
6. 先将基础信息插入至数据库,在得到评论信息后,根据索引 item_id 将其补充完整。
代码:
1 import requests 2 from lxml import etree 3 import pymongo 4 from concurrent import futures 5 6 7 class CrawlDog: 8 def __init__(self, keyword): 9 """ 10 初始化 11 :param keyword: 搜索的关键词 12 """ 13 self.keyword = keyword 14 self.mongo_client = pymongo.MongoClient(host='localhost') 15 self.mongo_collection = self.mongo_client['spiders']['jd'] 16 self.mongo_collection.create_index([('item_id', pymongo.ASCENDING)]) 17 18 def get_index(self, page): 19 """ 20 从搜索页获取相应信息并存入数据库 21 :param page: 搜索页的页码 22 :return: 商品的id 23 """ 24 url = 'https://search.jd.com/Search?keyword=%s&enc=utf-8&page=%d' % (self.keyword, page) 25 index_headers = { 26 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,' 27 'application/signed-exchange;v=b3', 28 'accept-encoding': 'gzip, deflate, br', 29 'Accept-Charset': 'utf-8', 30 'accept-language': 'zh,en-US;q=0.9,en;q=0.8,zh-TW;q=0.7,zh-CN;q=0.6', 31 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' 32 'Chrome/74.0.3729.169 Safari/537.36' 33 } 34 rsp = requests.get(url=url, headers=index_headers).content.decode() 35 rsp = etree.HTML(rsp) 36 items = rsp.xpath('//li[contains(@class, "gl-item")]') 37 for item in items: 38 try: 39 info = dict() 40 info['title'] = ''.join(item.xpath('.//div[@class="p-name p-name-type-2"]//em//text()')) 41 info['url'] = 'https:' + item.xpath('.//div[@class="p-name p-name-type-2"]/a/@href')[0] 42 info['store'] = item.xpath('.//div[@class="p-shop"]/span/a/text()')[0] 43 info['store_url'] = 'https' + item.xpath('.//div[@class="p-shop"]/span/a/@href')[0] 44 info['item_id'] = info.get('url').split('/')[-1][:-5] 45 info['price'] = item.xpath('.//div[@class="p-price"]//i/text()')[0] 46 info['comments'] = [] 47 self.mongo_collection.insert_one(info) 48 yield info['item_id'] 49 # 实际爬取过程中有一些广告, 其中的一些上述字段为空 50 except IndexError: 51 print('item信息不全, drop!') 52 continue 53 54 def get_comment(self, params): 55 """ 56 获取对应商品id的评论 57 :param params: 字典形式, 其中item_id为商品id, page为评论页码 58 :return: 59 """ 60 url = 'https://sclub.jd.com/comment/productPageComments.action?productId=%s&score=0&sortType=5&page=%d&' \ 61 'pageSize=10' % (params['item_id'], params['page']) 62 comment_headers = { 63 'Referer': 'https://item.jd.com/%s.html' % params['item_id'], 64 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' 65 'Chrome/74.0.3729.169 Safari/537.36' 66 } 67 rsp = requests.get(url=url, headers=comment_headers).json() 68 comments_count = rsp.get('productCommentSummary').get('commentCountStr') 69 comments = rsp.get('comments') 70 comments = [comment.get('content') for comment in comments] 71 self.mongo_collection.update_one( 72 # 定位至相应数据 73 {'item_id': params['item_id']}, 74 { 75 '$set': {'comments_count': comments_count}, # 添加comments_count字段 76 '$addToSet': {'comments': {'$each': comments}} # 将comments中的每一项添加至comments字段中 77 }, True) 78 79 def main(self, index_pn, comment_pn): 80 """ 81 实现爬取的函数 82 :param index_pn: 爬取搜索页的页码总数 83 :param comment_pn: 爬取评论页的页码总数 84 :return: 85 """ 86 # 爬取搜索页函数的参数列表 87 il = [i * 2 + 1 for i in range(index_pn)] 88 # 创建一定数量的线程执行爬取 89 with futures.ThreadPoolExecutor(15) as executor: 90 res = executor.map(self.get_index, il) 91 for item_ids in res: 92 # 爬取评论页函数的参数列表 93 cl = [{'item_id': item_id, 'page': page} for item_id in item_ids for page in range(comment_pn)] 94 with futures.ThreadPoolExecutor(15) as executor: 95 executor.map(self.get_comment, cl) 96 97 98 if __name__ == '__main__': 99 # 测试, 只爬取两页搜索页与两页评论 100 test = CrawlDog('耳机') 101 test.main(2, 2)
总结:爬取的过程中可能会被封 IP,测试时评论页面的获取被*,使用代理可以解决该问题,后面会来主要说一下代理的使用。