剛剛接觸爬蟲,花了一段時間研究了一下如何使用scrapy,寫了一個比較簡單的小程序,主要用於爬取京東商城有關進口牛奶頁面的商品信息,包括商品的名稱,價格,店鋪名稱,連接,以及評價的一些信息等。簡單記錄一下個人心得和體會,剛剛入門,可能理解的不夠深刻不夠抽象,不少東西也只是知其然不知其因此然,理解的仍是比較淺顯,但願有看見的大佬能一塊兒交流。html
先上我主要參考的幾篇博客,個人爬蟲基本上是在這兩篇博客的基礎上完成的,感謝大佬的無私分享:python
首先說明一下個人程序是基於以上二篇博客的基礎上進行修改的,主要的改動是針對3.6版本的python,修改了一些已經刪除的函數,修改了一些已經更新的頁面的網址,還有有些商品是京東全球購,商品頁面的信息和京東自營的不同,對此進行了斷定和處理等,並將信息輸出到Mysql中。mysql
整個爬蟲我已上傳至Github,歡迎你們討論交流。git
JDSpidergithub
scrapy爬蟲主要能夠分爲幾部分,以下圖所示:web
有關Scrapy的基本結構在第一篇博客裏也有所簡單說明,在此再也不贅述,若是須要了解更多仍是須要看官方文檔。在這裏我簡單說一下我認爲比較重要的幾個部分。正則表達式
Spider:這個部分能夠認爲是爬蟲的本體了,他的主要做用就是從下載好的內容中爬到你須要的東西,因此你在寫爬蟲的時候基本都是對Spider進行修改。sql
Item Pipeline:這個模塊簡單的說就是將你爬到的信息進行處理,輸出到Mysql等。所以在這裏須要完成python到Mysql的輸出。數據庫
在上面兩篇博客的基礎上對代碼進行了必定的修改,個人編程環境是Python 3.6,開發環境是win10下的Pycharm。須要注意的一點是,在IDE中進行爬蟲的運行和調試須要添加一些內容,若是是在IDE下進行運行的話,須要在項目的根目錄下添加一個名爲entrypoint的py文件,其中的代碼以下:express
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模塊編寫爬蟲時的一些心得了, 倉促完成的一篇博客,多有疏漏,本身理解不深的地方還有不少,繼續加油。