1. 程式人生 > 實用技巧 >C# 基於內容電影推薦專案(一)

C# 基於內容電影推薦專案(一)

從今天起,我將製作一個電影推薦專案,在此寫下部落格,記錄每天的成果。

其實,從我釋出C# 爬取貓眼電影資料這篇部落格後,

我就已經開始製作電影推薦專案了,今天寫下這篇部落格,也是因為專案進度已經完成50%了,我就想在這一階段停一下,回顧之前學到的知識。

一、主要為手機端

考慮到專案要有實用性,我選擇了手機端,電腦端用的人有點少。然後就是在 xamarin.Forms 和 xamarin.android 這兩個中做選擇了,我選擇了前者,因為xamarin.Forms 更接近WPF ,我也百度了一下,Forms完美支援MVVM設計模式,所以我果斷選擇了Forms。:)

二、Sql Server + WebApi + Xamarin.Forms

考慮到要釋出的話,程式直連資料庫肯定是不行的,所以我又臨時學習了WebApi,將前後端分離,WebApi讀取資料,以Json形式返回給App,APP讀取到Json資料後再顯示到介面上。

三、電影推薦實現思路

本人也是新手,不太會演演算法,但基於內容推薦還是比較簡單容易實現的:

想要基於內容,首先得找到電影有哪些屬性(標籤),下方圖中紅線標註的就是一部電影可以用的屬性,但我目前想要實現的推薦功能用不到這麼多屬性,所以我只將電影型別轉為特徵碼,

然後給每位使用者一個 LikeCode (喜好碼),再將喜好碼轉為特徵碼,放進資料庫中匹配,得到相似度最高的電影(MoviesN),MoviesN就是使用者喜歡的電影了,最後通過WebApi查出MoviesN,App通過WebApi的介面獲取MoviesN,App再顯示MoviesN,這樣,基於內容的電影推薦就完成了。

四、功能實現

前面說了一大堆,總得拿出點實際的東西給大家看看,

先介紹一下具體有哪些功能:

  1. 登入 --已完成
  2. 註冊 --未完成
  3. 首頁(上拉載入電影,按評分從高往低排序)--已完成
  4. 推薦頁(根據使用者喜好推薦相匹配的電影,如果沒有使用者喜好資料則隨機推薦)--未完成
  5. 相關電影頁(在使用者點選一部電影后,跳轉至該頁面,並向用戶推薦10部同型別的電影)--已完成
  6. 使用者頁(展示使用者的基本資訊和使用者喜好)--未完成
  7. 收集使用者喜好(使用者每點選一部電影,則向後臺傳送資料,使用者喜好該型別電影多一點)--未完成

下面我就將已完成功能的主要程式碼一段一段貼出來:

1、首頁

首頁目前就顯示兩個控制元件,一個是SearchBar,一個是ListView,主要看ListView怎樣實現的:

 <VM:InfiniteListView  ItemsSource="{Binding DisplayMovies}" LoadMoreCommand="{Binding LoadMoreCommand}" SelectedItem="{Binding SelectMovie}"
ItemClickCommand="{Binding MoviesItemClickCommand}"
RowHeight="200" SeparatorVisibility="None" HorizontalScrollBarVisibility="Never" VerticalScrollBarVisibility="Never">
<x:Arguments>
<ListViewCachingStrategy>RecycleElement</ListViewCachingStrategy>
</x:Arguments>
<VM:InfiniteListView.Footer>
<StackLayout Orientation="Vertical">
<Label Text="載入中……" HorizontalOptions="Center" TextColor="Gray" />
</StackLayout>
</VM:InfiniteListView.Footer>
<VM:InfiniteListView.ItemTemplate>
<DataTemplate>
<VM:MyCustomCell>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<!--Movie Image-->
<forms:CachedImage HeightRequest="200" Source="{Binding ImgSource}"/>
<!--<Image Grid.Column="0" Source="{Binding ImgSource}"/>-->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Text="{Binding DisplayName}" TextColor="Orange" FontSize="Large" Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2" HorizontalOptions="Start"/>
<Label Text="{Binding Detail}" Grid.Row="2" Grid.Column="0" Grid.RowSpan="4" Grid.ColumnSpan="2"/>
</Grid> </Grid>
</VM:MyCustomCell>
</DataTemplate>
</VM:InfiniteListView.ItemTemplate>
</VM:InfiniteListView>

這裡我自定義了一個InfiniteListView 可以實現上拉載入功能,通過自定義命令 LoadMoreCommand繫結到ViewModel中LoadMoreCommand 用於當滑動到最後一個Item時觸發載入方法,同時我也根據ItemTapped自定義了一個ItemClickCommand 用來監聽Item的點選。

/// <summary>
/// 自定義ListView,實現上拉載入
/// </summary>
public class InfiniteListView : ListView
{
/// <summary>
/// Load More
/// </summary>
public static readonly BindableProperty LoadMoreCommandProperty = BindableProperty.Create(nameof(LoadMoreCommand), typeof(DelegateCommand), typeof(InfiniteListView));
/// <summary>
/// Item Click
/// </summary>
public static BindableProperty ItemClickCommandProperty = BindableProperty.Create( nameof(ItemClickCommand),typeof(DelegateCommand), typeof(InfiniteListView)); public ICommand LoadMoreCommand
{
get { return (ICommand)GetValue(LoadMoreCommandProperty); }
set { SetValue(LoadMoreCommandProperty, value); }
} public ICommand ItemClickCommand
{
get { return (ICommand)this.GetValue(ItemClickCommandProperty); }
set { this.SetValue(ItemClickCommandProperty, value); }
}
public InfiniteListView( )
{
ItemAppearing += InfiniteListView_ItemAppearing;
this.ItemTapped += this.OnItemTapped;
}
public InfiniteListView(Xamarin.Forms.ListViewCachingStrategy strategy) : base(strategy)
{
ItemAppearing += InfiniteListView_ItemAppearing;
this.ItemTapped += this.OnItemTapped;
} private void OnItemTapped(object sender, ItemTappedEventArgs e)
{
if (e.Item != null && this.ItemClickCommand != null && this.ItemClickCommand.CanExecute(e))
{
this.ItemClickCommand.Execute(e.Item);
this.SelectedItem = null;
}
} /// <summary>
/// 當滑動到最後一個item時
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void InfiniteListView_ItemAppearing(object sender, ItemVisibilityEventArgs e)
{
var items = ItemsSource as ObservableCollection<MovieViewModel>; if (items != null && e.Item == items[items.Count - ])
{
if (LoadMoreCommand != null && LoadMoreCommand.CanExecute(null))
LoadMoreCommand.Execute(null);
}
}
}

接著是MyCustomCell ,這段程式碼是我從StackOverflow複製下來的,用於解決ViewCell中由圖片引起的卡頓掉幀問題。可以有效提高程式執行流暢性。

/// <summary>
/// 自定義ViewCell
/// </summary>
public class MyCustomCell : ViewCell
{
readonly CachedImage cachedImage = null; public MyCustomCell()
{
cachedImage = new CachedImage();
View = cachedImage;
} protected override void OnBindingContextChanged()
{
// you can also put cachedImage.Source = null; here to prevent showing old images occasionally
cachedImage.Source = null;
var item = BindingContext as MovieViewModel; if (item == null)
{
return;
} cachedImage.Source = item.ImgSource; base.OnBindingContextChanged();
}
}

LoadMoreCommand

 public DelegateCommand LoadMoreCommand
{
get
{
return new DelegateCommand
{
ExecuteAction = new Action<object>(LoadMoreFunc)
};
}
}
private void LoadMoreFunc(object parameter)
{
//Thread.Sleep(3000);
//MainPage.ThisPage.DisplayAlert("title",$"{CurrentIndex} - {DisplayMovies.Count}","ok");
new Thread(new ThreadStart(() => {
LoadMovies(GetIndex++ * , );
})).Start();
}
public void LoadMovies(int n,int m)
{
if (CurrentIndex >= moviesLimit10.Count)//Load More
{
Movies = MovieService.GetLimitNMMovies(n, m);
Movies2.Clear();
string[] DisStrs = new string[] { "主演:" , "上映日期:", "型別:", "評分:","\n" };
for (int i = ; i < Movies.Count; i++)
{
Movies2.Add(new MovieViewModel
{
//處理電影名,如果長度大於11則換行
DisplayName = Movies[i].Name.Length > ? Movies[i].Name.Insert(,"\n") : Movies[i].Name,
//處理ImrUrl,去掉@後面的符號
ImgSource = ImageSource.FromUri(new Uri(Movies[i].ImgUrl.Split('@')[])),
//Types = BinToStr(Movies[i].Types),//待優化
Detail = DisStrs[] + Movies[i].Stars + DisStrs[] +
//$"上映日期:{Movies[i].Time.ToString("yyyy-MM-dd")}\n" +
DisStrs[] + Movies[i].Time + DisStrs[] +
DisStrs[] + Common.MovieTypeDeserialization(Movies[i].Types) + DisStrs[] +//待優化
DisStrs[] + Movies[i].Score + DisStrs[],
Types = Common.MovieTypeDeserialization(Movies[i].Types),
Movie = Movies[i]
});
DisplayMovies.Add(Movies2[i]);
}
}
}

MoviesItemClickCommand

 public DelegateCommand MoviesItemClickCommand
{
get
{
return new DelegateCommand
{
ExecuteAction = new Action<object>(MoviesItemClickFunc)
};
}
} private void MoviesItemClickFunc(object parameter)
{
string[] typeArr = SelectMovie.Types.Split('/');
StaticSelectMovie = SelectMovie;
for (int i = ; i < typeArr.Length-; i++)//將使用者模型下與選中電影對應的型別資料加一
{
LoggedAccount.Account.UserLikeModel.LikeTypes[Common.MovieTypeSerialize(typeArr[i])]++;
}
//更新使用者喜好
int state = AccountService.UpdateUserLikeModel(LoggedAccount.Account.Account.ID,LoggedAccount.Account.UserLikeModel.LikeTypes); MainPage.ThisPage.Navigation.PushModalAsync(new MovieContentPage(SelectMovie.Movie.Name, SelectMovie.Movie.Types,SelectMovie.Types,SelectMovie.ImgSource));
}

2、相關電影頁

介面設計和主頁相差不大,主頁的InfiniteListView可以直接用到這個頁面。主要是如何獲取到相關電影。

獲取到相關電影需要將電影A的特徵碼與電影表中所有電影的特徵碼匹配,得出相似度大於0%的就算相關電影。

那麼問題來了,電影的特徵碼長啥樣?

這張圖是貓眼電影關於電影的分類

我將其做了一點小小的改動,將這些型別比作二進位制來看,得出一個長度為25的二進位制,型別“其他“為該二進位制的第一位,”愛情”就是二進位制中的第25位,圖中的序號我是用來計數的

然後將型別轉為一個長度為25位的二進位制,就得出了一部電影的特徵碼。再將這個特徵碼與所有電影的特徵碼相比較,做按位與運算,數值越高的就是相似度越高的電影,可是,這樣的話問題又來了,在Sql Server中 所有二進位制都會被轉為16進位制來表達

所以,我得自己寫函式(DBO.MatchMoves),將傳入的數字A,B作比較,先轉為varchar 型別,然後判斷相同下標下值相等且值為1的情況有幾個,並得出 len,再套入公式 :(len / A含有數字1的個數) = 相似度,這樣就完成了一次相似度匹配。

最後根據函式寫出的查詢語句則為:

這樣,功能就完成一大半了,剩下的就是通過WebApi查出資料,App呼叫介面獲取資料,再顯示資料,就完事了,具體操作與主頁的程式碼有一定的重複度,我就不貼出來了。

五、效果圖

1、主頁

2、相關電影

以上,就是我目前開發的成果,過幾天我再更新成果。

完。