數據清洗(二):崗位職責與要求的分離


    在現有的所有互聯網招聘網站上,崗位信息里的所有條目都是在同一級標簽下。因此,崗位信息作為一個整體,就需要額外的操作把要求與職責分離開。鑒於崗位信息里數據格式的不統一,因此博主放棄了使用正則表達式的方法,而是選擇了模糊匹配+結構化匹配,將字符串比較的問題轉化成了概率問題。

 

一、數據存儲結構

    在之前寫的爬蟲里,崗位信息一欄使用Xpath的String()方法抓取,作為一個大的字符串,所有信息都位於一個單元格中。現在計划在爬蟲運行時,得到崗位信息后就將其分離,再寫入硬盤中。所以,爬取數據時的格式會極大的影響分離的方法,字符串適合使用正則表達式,但是在格式混亂的崗位信息中,這顯然不是完美的解法,如'崗位職責',與之類似的還有'工作內容','職位描述'等等,這些詞的各種排列組合會極大的增加正則表達式的長度。

    所以我決定將每一行信息都轉化為數組的一個元素,再通過上下文信息與其自身的詞匯信息判斷其歸屬。在我爬取的51job移動端中,崗位信息的條目都在標簽<article>下,因此使用//text()方法,將<article>標簽下每一行的信息都轉化為一個數組元素。

  1. info = selector.xpath('//*[@id="pageContent"]/div[3]/div[2]/article//text()')  

 

 

圖表 1 數據在源碼中的位置

 

二、數據的上下文關系

    上圖所示的數據格式是最完美的,只需要正則表達式就能匹配成功。每一條數據都含有信息,'崗位職責'與'崗位要求'預示下文的數據與這個主題相關,其余信息則屬於某一個主題。所以對這類結構化非常明顯的信息,只需要匹配出'職責'與'要求'即可完成數據的分離。

    另一種情況如下所示,職位描述里包含了一眼就能看出來的崗位職責與要求,但是職責頭信息缺失,通過上下文無法得出該信息的歸屬。因此,對於這類上下文無關的信息,就需要單獨進行處理。

圖表 2 缺少主題的jd

    通過上述分析,就得出了這樣一個處理流程:如果現在處理的信息屬於頭部信息(職責、要求等),則進入結構化處理流程,否則單獨處理。

圖表 3 流程圖

 

三、模糊匹配

    由於對相同意思的不同表述,以及輸入過程中可能會出現的錯誤,因此使用模糊匹配來近似地查找與字符串匹配的字串。

    字符串模糊匹配( fuzzy string matching)是一種近似地(而不是精確地)查找與模式匹配的字符串的技術。換句話說,字符串模糊匹配是一種搜索,即使用戶拼錯單詞或只輸入部分單詞進行搜索,也能夠找到匹配項。因此,它也被稱為字符串近似匹配。

    先導入第三方庫fuzzywuzzy:

  1. from fuzzywuzzy import fuzz  

    fuzz有五個常用的函數,先做一個簡單的測試來看看區別。

    函數功能就像它們的名稱一樣,通俗易懂。再換一個長一點兒的:

    所以我選擇partial_token_sort_ratio()的值作為判斷的依據。

通過分析ratio函數的源碼,可以發現ratio()函數的求值公式:

M是匹配的元素個數,T是字符串長度。所以針對partial的函數,我們可以用2/(len(thisStr))*100來判斷字符串是否滿足模糊匹配。

另外有一個小問題,

    后來發現,str2 = 'word1word2word3'時,word1出現在str1中,則匹配失敗。所以在str2前加入一個字,取'工作'后一字,則解決問題。

 

四、模糊匹配自定義數據集

    正如正則表達式需要自己定義匹配的字符一樣,模糊匹配也需要自己定義一個類似的字符集。我們總共需要四個字符集,分別是崗位要求與職責頭的字符集,以及具體要求的字符集。

  1. str_responsibility = '作職責描述介紹內容'  
  2. str_requirement = '能力要求需求資格條件標准'  
  3. str_line_res = '負責基於構建根據制定規范需求'  
  4. str_line_req = '經驗熟悉熟練掌握精通優先學歷專業以上基礎知識學習交流年齡編程了解'  

    

五、代碼實現

    函數parse()作為信息處理的入口,接收一個含有崗位信息的數組,返回一個數組,數組元素分別是崗位職責與要求。

    由於爬取的信息含有大量制表符與空字符,所以需要排除無效的信息,並用一個新的數組'ls_jd'存儲崗位信息。

  1. def parse(ls):  
  2.     if len(ls) == 0:  
  3.         return ['null','null']  
  4.     ls_jd = []  
  5.     result_res = []  
  6.     result_req = []  
  7.     str_responsibility = '作職責描述介紹內容'  
  8.     str_requirement = '能力要求需求資格條件標准'  
  9.     for i in range(len(ls)):  
  10.         str_line = str(ls[i]).strip()  
  11.         if len(str_line.strip()) < 2:  
  12.             continue  
  13.         else:  
  14.             ls_jd.append(str_line)  

    再聲明一個變量Index,用來記錄現在讀取到數組元素的下標。

    使用一個循環,從第一個元素開始,依次讀取數組元素,並求得其與'崗位職責'字串、'崗位要求'字串的相似度,再分別進行匹配。

    另外,由於需要對循環元素進行操作,所以不能使用for循環,因此此處使用了while()。

  15. index = 0  
  16. while int(index) < len(ls_jd):  
  17.     str_line = ls_jd[index]  
  18.     if len(str_line) < 10:  
  19.         fuzz_res = fuzz.partial_token_sort_ratio(str_line, str_responsibility)  
  20.         fuzz_req = fuzz.partial_token_sort_ratio(str_line, str_requirement)  
  21.     else:  
  22.         parse_line(str_line,result_res,result_req)  
  23.         index += 1  
  24.         continue  
  25.     if fuzz_res > fuzz_req and fuzz_res >= (2/len(str_responsibility)*100):  
  26.         index = parse_res(index,ls_jd,str_requirement,result_res)  
  27.     elif fuzz_req > fuzz_res and fuzz_req >= (2/len(str_requirement)*100):  
  28.         index = parse_req(index,ls_jd,str_responsibility,result_req)  
  29.     else:  
  30.         print('warn: '+ str_line)  
  31.     index += 1  

    基於結構的信息提取會修改當前讀取數組的下標,所以需要將當前函數內讀取到的下標返回。參數里傳列表,實際上傳的是地址,所以結果不需要額外操作。

  32. def parse_res(index,ls,str_break,result_res):  
  33.     # print('崗位職責()Start')  
  34.     while index < len(ls)-1:  
  35.         index += 1  
  36.         fuzz_break = fuzz.partial_token_sort_ratio(ls[index], str_break)  
  37.         if fuzz_break < 49:  
  38.             result_res.append(ls[index])  
  39.         else:  
  40.             return index-1  
  41.     return index  
  42. def parse_req(index,ls,str_break,result_req):  
  43.     # print('崗位要求()Start')  
  44.     while index < len(ls)-1:  
  45.         index += 1  
  46.         fuzz_break = fuzz.partial_token_sort_ratio(ls[index], str_break)  
  47.         if fuzz_break < 49:  
  48.             result_req.append(ls[index])  
  49.         else:  
  50.             return index-1  
  51.     return index  
  52. def parse_line(line,result_res,result_req):  
  53.     str_res = '負責基於構建根據制定規范需求'  
  54.     str_req = '經驗熟悉熟練掌握精通優先學歷專業以上基礎知識學習交流年齡編程了解'  
  55.     fuzz_res = fuzz.partial_token_sort_ratio(line, str_res)  
  56.     fuzz_req = fuzz.partial_token_sort_ratio(line, str_req)  
  57.     if fuzz_res-1 >= (2/len(str_res)*100):  
  58.         result_res.append(line)  
  59.     elif fuzz_req-1 >= (2/len(str_req)*100):  
  60.         result_req.append(line)  

 

六、結果

    可以看出還是有一些小問題的。

七、改進與設想

    基於概率匹配的信息分解受制於自定義匹配數據的完整性,盡管目前給出的幾個關鍵詞囊括了大部分的情況,不過仍然有相當多的漏網之魚。

    另外,fuzz庫的模糊匹配並不能完美適配此次字符匹配,除了第三大點后給出的問題外,對長句中不同詞語應該有不同的權重,以避免長居中詞太多導致匹配失敗。

    不過最可喜的應該是,這次有了相對統一的數據,回頭可以用這些數據去Spark上跑個模型,親自操刀一下機器學習了,哈哈哈。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM