剛剛接觸爬蟲,花了一段時間研究了一下如何使用scrapy,寫了一個比較簡單的小程序,主要用於爬取京東商城有關進口牛奶頁面的商品信息,包括商品的名稱,價格,店鋪名稱,鏈接,以及評價的一些信息等。簡單記錄一下我的心得和體會,剛剛入門,可能理解的不夠深入不夠抽象,很多東西也只是知其然不知其所以然,理解的還是比較淺顯,希望有看見的大佬能一起交流。
先上我主要參考的幾篇博客,我的爬蟲基本上是在這兩篇博客的基礎上完成的,感謝大佬的無私分享:
小白進階之Scrapy第一篇
scrapy爬取京東商城某一類商品的信息和評論(一)
首先說明一下我的程序是基於以上二篇博客的基礎上進行修改的,主要的改動是針對3.6版本的python,修改了一些已經刪除的函數,修改了一些已經更新的頁面的網址,還有有些商品是京東全球購,商品頁面的信息和京東自營的不一樣,對此進行了判定和處理等,並將信息輸出到Mysql中。
整個爬蟲我已上傳至Github,歡迎大家討論交流。
scrapy爬蟲主要可以分為幾部分,如下圖所示:
有關Scrapy的基本結構在第一篇博客里也有所簡單說明,在此不再贅述,如果需要了解更多還是需要看官方文檔。在這里我簡單說一下我認為比較重要的幾個部分。
Spider:這個部分可以認為是爬蟲的本體了,他的主要作用就是從下載好的內容中爬到你需要的東西,所以你在寫爬蟲的時候基本都是對Spider進行修改。
Item Pipeline:這個模塊簡單的說就是將你爬到的信息進行處理,輸出到Mysql等。因此在這里需要完成python到Mysql的輸出。
在上面兩篇博客的基礎上對代碼進行了一定的修改,我的編程環境是Python 3.6,開發環境是win10下的Pycharm。需要注意的一點是,在IDE中進行爬蟲的運行和調試需要添加一些內容,如果是在IDE下進行運行的話,需要在項目的根目錄下添加一個名為entrypoint的py文件,其中的代碼如下:
from scrapy.cmdline import execute execute(['scrapy','crawl','JDSpider']) #用於在IDE里運行
其中JDSpider即是你自定義的Spider的name屬性,注意一定要與Spider的名字匹配。
如果要在IDE下進行調試的話,則需要在與setting.py的目錄下添加一個名為run.py的文件,文件的代碼如下:
# -*- coding: utf-8 -*- from scrapy import cmdline name = 'JDSpider' cmd = 'scrapy crawl {0}'.format(name) cmdline.execute(cmd.split()) #用於在IDE里進行Debug
需要運行爬蟲的時候,直接運行entrypoiot.py即可,同理,進行調試的時候debug entrypoint.py。
下面開始進行爬蟲的編寫了。第一步,先確定你需要進行爬取的信息都有那些,那么我們先來編寫items.py。代碼如下:
import scrapy class JDSpiderItem(scrapy.Item): # define the fields for your item here like: ID = scrapy.Field() # 商品ID name = scrapy.Field() # 商品名字 comment = scrapy.Field() # 評論人數 shop_name = scrapy.Field() # 店家名字 price = scrapy.Field() # 價錢 link = scrapy.Field() comment_num = scrapy.Field() score1count = scrapy.Field() # 評分為1星的人數 score2count = scrapy.Field() # 評分為2星的人數 score3count = scrapy.Field() # 評分為3星的人數 score4count = scrapy.Field() # 評分為4星的人數 score5count = scrapy.Field()
這一部分比較簡單,只要將你想要爬取的信息提供一個Scrapy.Field()方法即可。
第二部分的內容是編寫爬蟲的設置,修改settings.py中的代碼。
MYSQL_HOSTS = "127.0.0.1" MYSQL_USER = "root" MYSQL_PASSWORD = "7911upup" MYSQL_PORT = 3306 MYSQL_DB = "JD_test" # HTTPCACHE_ENABLED = True # HTTPCACHE_EXPIRATION_SECS = 0 # HTTPCACHE_DIR = 'httpcache' # HTTPCACHE_IGNORE_HTTP_CODES = [] # HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage' # DOWNLOAD_DELAY = 7 # 下載延遲
其中第一部分的內容是有關Mysql的接口,127.0.0.1是本機的保留地址,root是Mysql數據庫的賬戶名稱,第三行是密碼,第四行是端口,默認為3306,第五行是mysql建立的database名稱。
第二部分是本地緩存,如果取消注釋的話是建立本地緩存,這樣能夠減少網站壓力,也方便進行調試,我一開始在調試的過程中是保留本地緩存的,但是在進行調試的過程中發現經過一段時間的調試之后發生了數據丟失的現象,不知道是不是跟我的程序編寫有關系,所以我個人建議如果是剛開始進行調試的時候盡可能的減少爬取的數據量,並不使用本地的緩存,這樣能夠防止數據出現錯誤,便與調試。
既然剛才提到了Mysql,這里也簡單說一下mysql的操作吧,由於我對這一塊不太了解,在這里也不獻丑了,直接上代碼,看代碼還是比較好理解的,就是首先建立一個database,然后在其中建立一個table,然后再設置一些變量的名稱和類型。
#create database JD_test character set gbk; use JD_test; DROP TABLE IF EXISTS `JD_name`; CREATE TABLE `JD_name` ( `id` int(11) NOT NULL AUTO_INCREMENT, `good_id` varchar(255) DEFAULT NULL, `name` varchar(255) DEFAULT NULL, `price` varchar(255) DEFAULT NULL, `comment` varchar(255) DEFAULT NULL, `shop_name` varchar(255) DEFAULT NULL, `link` varchar(255) DEFAULT NULL, `score1count` varchar(255) DEFAULT NULL, `score2count` varchar(255) DEFAULT NULL, `score3count` varchar(255) DEFAULT NULL, `score4count` varchar(255) DEFAULT NULL, `score5count` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8mb4; truncate JD_name;
至於python這一部分,在3.6中是用到了pymysql這個庫完成二者的連接的。這一部分的代碼如下。
import pymysql.connections import pymysql.cursors MYSQL_HOSTS = "127.0.0.1" MYSQL_USER = "root" MYSQL_PASSWORD = "7911upup" MYSQL_PORT = 3306 MYSQL_DB = "JD_test" connect = pymysql.Connect( host = MYSQL_HOSTS, port = MYSQL_PORT, user = MYSQL_USER, passwd = MYSQL_PASSWORD, database = MYSQL_DB, charset="utf8" ) cursor = connect.cursor() # # 插入數據 class Sql: @classmethod def insert_JD_name(cls,id, name, shop_name, price, link, comment_num ,score1count, score2count, score3count, score4count, score5count): sql = "INSERT INTO jd_name (good_id, name, comment, shop_name, price, link ,score1count, score2count," \ " score3count, score4count, score5count) VALUES ( %(id)s, %(name)s, %(comment_num)s, %(shop_name)s, %(price)s" \ ", %(link)s, %(score1count)s, %(score2count)s, %(score3count)s, %(score4count)s, %(score5count)s )" value = { 'id' : id, 'name' : name, 'comment' : comment_num, 'shop_name' : shop_name, 'price' : price, 'link' : link, 'comment_num' : comment_num, 'score1count' : score1count, 'score2count' : score2count, 'score3count' : score3count, 'score4count' : score4count, 'score5count' : score5count, } cursor.execute(sql, value) connect.commit()
接下來就是Spider的編寫了,在Spider類中有幾個比較重要的變量和函數,一個是start_url,這個是爬蟲開始爬取的網站地址,由於在JD首頁進行搜索顯示的頁面是30條動態加載的,所以爬取不是特別方便,所以選取在首頁左側中的進口牛奶分類的頁面,該頁面能夠直接顯示60條商品數據。網址為https://list.jd.com/list.html?cat=1320,5019,12215&page=N&sort=sort_totalsales15_desc&trans=1&JL=6_0_0#J_main。這里N即為具體的頁碼,通過如下代碼將start_url設置成為一個list。
start_urls = [] for i in range(1, 10+1): # 這里需要自己設置頁數 url = 'https://list.jd.com/list.html?cat=1320,5019,12215&page='+ str(i)+'&sort=sort_totalsales15_desc&trans=1&JL=6_0_0#J_main' start_urls.append(url)
第二個比較重要的函數是parse,在這里我們素質四連,一共有parse,parse_detail,parse_getCommentnum,parse_price四個方法,parse用來爬取商品的ID,鏈接,還有商品的名稱;parse_detail用來爬取商品的店鋪名,后面兩個方法則是用來爬取評論數和不同評價的人數以及商品的價格。
解析數據的話,可以用Xpath直接解析,也可以用導入的BS4等庫來做,在這里我用Xpath+正則表達式的一套combo來完成,不懂的老哥可以先看一下這個有關正則表達式的介紹。相比於我參考的代碼,在網站解析這一部分很多解析的代碼已經失效了,年久失修只能我自己動手來修改,剛開始上手確實有點麻煩,畢竟沒有JS基礎,看網頁源代碼有些吃力,后來操作了一番以后也就有點熟悉了,簡單介紹一下如何查找你需要的元素。
我采用的是獵豹瀏覽器,是基於Chrome內核的,調試起來應該跟Chrome沒什么區別,首先在對應的頁面單擊F12,出現如下頁面:
首先進行觀察,可以看出所有的商品都有一個class=‘gl-item’的標簽,再單擊所示圖標,將光標移動到你需要的信息上點右鍵,例如某一個商品的名稱哪里,即可在右邊顯示出對應的信息,從圖中可以知道這個商品名稱的信息是在 li//div/div[@class="p-name"]/a/em/ 的text中,同時也可以看出其中的文本還包括一些空格等等,所以需要使用正則表達式對其進行篩選。這里的代碼如下:
def parse(self, response): # 解析搜索頁 # print(response.text) sel = Selector(response) # Xpath選擇器 goods = sel.xpath('//li[@class="gl-item"]') for good in goods: item1 = JDSpiderItem() temp1 = str(good.xpath('./div/div[@class="p-name"]/a/em/text()').extract()) pattern = re.compile("[\u4e00-\u9fa5]+.+\w") #從第一個漢字起 匹配商品名稱 good_name = re.search(pattern,temp1) item1['name'] = good_name.group() item1['link'] = "http:" + str(good.xpath('./div/div[@class="p-img"]/a/@href').extract())[2:-2] item1['ID'] = good.xpath('./div/@data-sku').extract() if good.xpath('./div/div[@class="p-name"]/a/em/span/text()').extract() == ['全球購']: item1['link'] = 'https://item.jd.hk/' + item1['ID'][0] +'.html' url = item1['link'] + "#comments-list" yield scrapy.Request(url, meta={'item': item1}, callback=self.parse_detail)
簡單的說一下幾個需要注意的地方,一個是正則表達式中,[\u4e00-\u9fa5]+從第一個漢字開始匹配,這里其實是有一點小BUG的,因為有的商品名稱是以字符和數字或者標點符號開頭的,由於我爬取的商品信息第一頁里沒有這種情況,所以我也沒有修改,后面應該進行適當的調整,修改一下這個正則表達式。第二個是注意re模塊中search和match的區別,match是從第一個字符開始進行匹配,而search是在整個字符串中進行匹配,建議使用search。第三個需要注意的地方是對於牛奶這種商品,分為兩個類型,一個是JD自營的或者第三方的一些店鋪,這些網址是類似的,而還有一種是京東全球購,這種商品的網址跟之前的是不一樣的,網址開頭是items.jd.hk。因此在爬的過程中要將全球購的這個標簽給選取出來,針對不同的商品類型,對link的值進行修改,這樣傳遞給request才是有效的url。
parse_detail這個函數是用於爬取商品的店鋪名的,這里進入了商品的詳情頁面,url是通過parse函數抓取的ID生成的,全球購和國內商品的url不同,在這里對於店鋪的抓取也是不同的,其中的標簽是不一樣的,需要注意的就是有的商品是京東自營的,沒有具體的店鋪名,在這里需要進行判別。
def parse_detail(self, response): # pass item1 = response.meta['item'] sel = Selector(response) # Xpath選擇器 if response.url[:18] == 'https://item.jd.hk': #判斷是否為全球購 goods = sel.xpath('//div[@class="shopName"]') temp = str(goods.xpath('./strong/span/a/text()').extract())[2:-2] if temp == '': item1['shop_name'] = '全球購:'+ 'JD全球購' #判斷是否JD自營 else: item1['shop_name'] = '全球購:' + temp # print('全球購:'+ item1['shop_name']) else: goods = sel.xpath('//div[@class="J-hove-wrap EDropdown fr"]') item1['shop_name'] = str(goods.xpath('./div/div[@class="name"]/a/text()').extract())[2:-2] if item1['shop_name'] == '': #是否JD自營 item1['shop_name'] = '京東自營' # print(item1['shop_name'])
下面的兩個parse函數沒有太多的改動,與第二篇博客中的相差無幾,只是把其中解析的網址做了替換,之前的不能用了。在此也不多說了,煩請各位移步那篇博客。我就只上個代碼了。
def parse_price(self, response): item1 = response.meta['item'] temp1 = str(response.body).split('jQuery712392([') s = temp1[1][:-6] # 獲取到需要的json內容 js = json.loads(str(s)) # js是一個list item1['price'] = js['p'] return item1 def parse_getCommentnum(self, response): item1 = response.meta['item'] js = json.loads(str(response.body)[2:-1]) item1['score1count'] = js['CommentsCount'][0]['Score1Count'] item1['score2count'] = js['CommentsCount'][0]['Score2Count'] item1['score3count'] = js['CommentsCount'][0]['Score3Count'] item1['score4count'] = js['CommentsCount'][0]['Score4Count'] item1['score5count'] = js['CommentsCount'][0]['Score5Count'] item1['comment_num'] = js['CommentsCount'][0]['CommentCount'] num = item1['ID'] # 獲得商品ID s1 = re.findall("\d+",str(num))[0] url = "http://p.3.cn/prices/mgets?callback=jQuery712392&type=1&area=1_2800_2849_0.138365810&pdtk=&pduid=15083882680322055841740&pdpin=jd_4fbc182f7d0c0&pin=jd_4fbc182f7d0c0&pdbp=0&skuIds=J_" + s1 yield scrapy.Request(url, meta={'item': item1}, callback=self.parse_price)
最后的部分就是pipeline,這里完成對爬取的數據的輸出,輸出到mysql中。
class JdspiderPipeline(object): def process_item(self, item, spider): if isinstance(item, JDSpiderItem): good_id = item['ID'] good_name = item['name'] shop_name = item['shop_name'] price = item['price'] link = item['link'] comment_num = item['comment_num'] score1count = item['score1count'] score2count = item['score2count'] score3count = item['score3count'] score4count = item['score4count'] score5count = item['score5count'] Sql.insert_JD_name(good_id, good_name, shop_name, price, link, comment_num ,score1count, score2count, score3count, score4count, score5count) # print('存儲一條信息完畢了哦') return item
在Mysql輸出到csv中時會出現一個問題,即輸出的中文會出現亂碼,在這里提供一個解決方案,將輸出的csv文件以記事本的形式打開,另存為csv的時候可以選擇以utf-8進行存儲,然后再打開即可。
成品圖如下:
以上就是我關於Scrapy模塊編寫爬蟲時的一些心得了, 倉促完成的一篇博客,多有疏漏,自己理解不深的地方還有很多,繼續加油。